pi-cursor-sdk 0.1.16 → 0.1.18
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.
- package/CHANGELOG.md +53 -1
- package/README.md +2 -2
- package/docs/cursor-live-smoke-checklist.md +54 -41
- package/docs/cursor-model-ux-spec.md +4 -3
- package/docs/cursor-testing-lessons.md +199 -0
- package/package.json +14 -5
- package/scripts/isolated-cursor-smoke.sh +226 -0
- package/scripts/steering-rpc-smoke.mjs +238 -0
- package/scripts/tmux-live-smoke.sh +418 -0
- package/scripts/validate-smoke-jsonl.mjs +207 -0
- package/src/cursor-context-tools.ts +6 -0
- package/src/cursor-display-text.ts +10 -0
- package/src/cursor-edit-diff.ts +11 -0
- package/src/cursor-env-boolean.ts +22 -0
- package/src/cursor-live-run-coordinator.ts +483 -0
- package/src/cursor-native-replay-routing.ts +48 -0
- package/src/cursor-native-replay-trace.ts +29 -0
- package/src/cursor-native-tool-display-registration.ts +103 -0
- package/src/cursor-native-tool-display-replay.ts +465 -0
- package/src/cursor-native-tool-display-state.ts +78 -0
- package/src/cursor-native-tool-display-tools.ts +102 -0
- package/src/cursor-native-tool-display.ts +10 -648
- package/src/cursor-partial-content-emitter.ts +121 -0
- package/src/cursor-pi-tool-bridge-abort.ts +133 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +179 -0
- package/src/cursor-pi-tool-bridge-mcp.ts +118 -0
- package/src/cursor-pi-tool-bridge-run.ts +384 -0
- package/src/cursor-pi-tool-bridge-server.ts +182 -0
- package/src/cursor-pi-tool-bridge-snapshot.ts +88 -0
- package/src/cursor-pi-tool-bridge-types.ts +80 -0
- package/src/cursor-pi-tool-bridge.ts +42 -1104
- package/src/cursor-provider-live-run-drain.ts +405 -0
- package/src/cursor-provider-turn-coordinator.ts +460 -0
- package/src/cursor-provider.ts +77 -1103
- package/src/cursor-question-tool.ts +9 -1
- package/src/cursor-record-utils.ts +26 -0
- package/src/cursor-sdk-output-filter.ts +100 -0
- package/src/cursor-sensitive-text.ts +37 -0
- package/src/cursor-tool-transcript.ts +28 -1229
- package/src/cursor-transcript-tool-formatters.ts +641 -0
- package/src/cursor-transcript-tool-specs.ts +441 -0
- package/src/cursor-transcript-utils.ts +276 -0
|
@@ -1,648 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
getLanguageFromPath,
|
|
12
|
-
highlightCode,
|
|
13
|
-
type ExtensionAPI,
|
|
14
|
-
type ExtensionContext,
|
|
15
|
-
type ExtensionHandler,
|
|
16
|
-
type SessionStartEvent,
|
|
17
|
-
type ToolDefinition,
|
|
18
|
-
} from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import { Image, Text, type Component } from "@earendil-works/pi-tui";
|
|
20
|
-
import { Type, type TSchema } from "typebox";
|
|
21
|
-
import { getCursorSessionCwd } from "./cursor-session-cwd.js";
|
|
22
|
-
import {
|
|
23
|
-
CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
|
|
24
|
-
CURSOR_REPLAY_LEGACY_TOOL_NAMES,
|
|
25
|
-
getCursorReplayDisplayLabel,
|
|
26
|
-
getCursorReplaySourceToolName,
|
|
27
|
-
isCursorReplayToolName,
|
|
28
|
-
type CursorReplayToolName,
|
|
29
|
-
} from "./cursor-tool-names.js";
|
|
30
|
-
import type { CursorPiToolDisplay } from "./cursor-tool-transcript.js";
|
|
31
|
-
|
|
32
|
-
const CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME] as const;
|
|
33
|
-
const CURSOR_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME, ...CURSOR_REPLAY_LEGACY_TOOL_NAMES] as const;
|
|
34
|
-
const NATIVE_CURSOR_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls", ...CURSOR_REPLAY_TOOL_NAMES] as const;
|
|
35
|
-
type NativeCursorToolName = (typeof NATIVE_CURSOR_TOOL_NAMES)[number];
|
|
36
|
-
const NATIVE_CURSOR_TOOL_DISPLAY_ENV = "PI_CURSOR_NATIVE_TOOL_DISPLAY";
|
|
37
|
-
// Registration-only kill switch for users who want transcript fallback without shadowing read/bash/ls.
|
|
38
|
-
const NATIVE_CURSOR_TOOL_REGISTRATION_ENV = "PI_CURSOR_REGISTER_NATIVE_TOOLS";
|
|
39
|
-
const CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES = 8;
|
|
40
|
-
const cursorReplayToolSchema = Type.Object({}, { additionalProperties: true });
|
|
41
|
-
|
|
42
|
-
export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
|
|
43
|
-
id: string;
|
|
44
|
-
terminate?: boolean;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const registeredNativeToolNames = new Set<NativeCursorToolName>();
|
|
48
|
-
const nativeToolResults = new Map<string, CursorNativeToolDisplayItem>();
|
|
49
|
-
|
|
50
|
-
type CursorNativeToolRegistryApi = Pick<ExtensionAPI, "getActiveTools" | "getAllTools" | "registerTool" | "setActiveTools">;
|
|
51
|
-
|
|
52
|
-
interface CursorNativeToolDisplayExtensionApi extends CursorNativeToolRegistryApi {
|
|
53
|
-
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
54
|
-
on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function readBooleanEnv(name: string): boolean | undefined {
|
|
58
|
-
const value = process.env[name]?.trim().toLowerCase();
|
|
59
|
-
if (value === "1" || value === "true" || value === "yes" || value === "on") return true;
|
|
60
|
-
if (value === "0" || value === "false" || value === "no" || value === "off") return false;
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isCursorNativeToolDisplayRequested(): boolean {
|
|
65
|
-
const override = readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV);
|
|
66
|
-
if (override !== undefined) return override;
|
|
67
|
-
return process.stdout.isTTY === true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function isNativeCursorToolName(toolName: string): toolName is NativeCursorToolName {
|
|
71
|
-
return NATIVE_CURSOR_TOOL_NAMES.some((nativeToolName) => nativeToolName === toolName);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
function isCursorNativeToolRegistrationRequested(): boolean {
|
|
76
|
-
return readBooleanEnv(NATIVE_CURSOR_TOOL_REGISTRATION_ENV) !== false && isCursorNativeToolDisplayRequested();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function isCursorNativeToolDisplayEnabled(): boolean {
|
|
80
|
-
return registeredNativeToolNames.size > 0;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function isCursorNativeToolDisplayRuntimeEnabled(): boolean {
|
|
84
|
-
return isCursorNativeToolDisplayRequested() && registeredNativeToolNames.size > 0;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function canRenderCursorToolNatively(toolName: string): boolean {
|
|
88
|
-
return isNativeCursorToolName(toolName) && registeredNativeToolNames.has(toolName);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function recordCursorNativeToolDisplay(item: CursorNativeToolDisplayItem): boolean {
|
|
92
|
-
if (!canRenderCursorToolNatively(item.toolName)) return false;
|
|
93
|
-
nativeToolResults.set(item.id, item);
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function deleteCursorNativeToolDisplay(id: string): void {
|
|
98
|
-
nativeToolResults.delete(id);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function consumeCursorNativeToolDisplay(id: string): CursorNativeToolDisplayItem | undefined {
|
|
102
|
-
const item = nativeToolResults.get(id);
|
|
103
|
-
if (item) nativeToolResults.delete(id);
|
|
104
|
-
return item;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function isCursorReplayToolCallId(toolCallId: string): boolean {
|
|
108
|
-
return toolCallId.startsWith("cursor-replay-");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function isCursorFileMutationToolName(toolName: string): toolName is "edit" | "write" {
|
|
112
|
-
return toolName === "edit" || toolName === "write";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export const __testUtils = {
|
|
116
|
-
nativeToolResultCount: () => nativeToolResults.size,
|
|
117
|
-
reset(): void {
|
|
118
|
-
registeredNativeToolNames.clear();
|
|
119
|
-
nativeToolResults.clear();
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
|
|
124
|
-
definition: ToolDefinition<TParams, TDetails, TState>,
|
|
125
|
-
getCurrentDefinition: () => ToolDefinition<TParams, TDetails, TState>,
|
|
126
|
-
): ToolDefinition<TParams, TDetails, TState> {
|
|
127
|
-
return {
|
|
128
|
-
...definition,
|
|
129
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
130
|
-
const cursorDisplay = consumeCursorNativeToolDisplay(toolCallId);
|
|
131
|
-
if (cursorDisplay) {
|
|
132
|
-
if (cursorDisplay.isError) {
|
|
133
|
-
const text = cursorDisplay.result.content
|
|
134
|
-
.map((entry) => (entry.type === "text" ? entry.text : undefined))
|
|
135
|
-
.filter((entry): entry is string => Boolean(entry))
|
|
136
|
-
.join("\n");
|
|
137
|
-
throw new Error(text || "Cursor tool replay failed");
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
content: cursorDisplay.result.content,
|
|
141
|
-
details: cursorDisplay.result.details as TDetails,
|
|
142
|
-
terminate: cursorDisplay.terminate ?? true,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
if (isCursorFileMutationToolName(definition.name) && isCursorReplayToolCallId(toolCallId)) {
|
|
146
|
-
throw new Error(`No recorded Cursor ${definition.name} result was available. This replay-only call does not execute file mutations.`);
|
|
147
|
-
}
|
|
148
|
-
return getCurrentDefinition().execute(toolCallId, params, signal, onUpdate, ctx);
|
|
149
|
-
},
|
|
150
|
-
renderCall(args, theme, context) {
|
|
151
|
-
if (isCursorFileMutationToolName(definition.name) && isCursorReplayToolCallId(context.toolCallId)) {
|
|
152
|
-
return renderNativeLookingCursorFileMutationCall(definition.name, args as Record<string, unknown>, theme, context.isPartial);
|
|
153
|
-
}
|
|
154
|
-
const currentRenderCall = getCurrentDefinition().renderCall;
|
|
155
|
-
return currentRenderCall ? currentRenderCall(args, theme, context) : new Text("", 0, 0);
|
|
156
|
-
},
|
|
157
|
-
renderResult(result, options, theme, context) {
|
|
158
|
-
const details = asCursorReplayToolDetails(result.details);
|
|
159
|
-
if (isCursorFileMutationToolName(definition.name) && details?.cursorToolName === definition.name) {
|
|
160
|
-
return renderCursorReplayResult(result, options, theme, context, context.isError);
|
|
161
|
-
}
|
|
162
|
-
const currentRenderResult = getCurrentDefinition().renderResult;
|
|
163
|
-
return currentRenderResult ? currentRenderResult(result, options, theme, context) : new Text("", 0, 0);
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
interface CursorReplayToolDetails {
|
|
169
|
-
cursorToolName?: string;
|
|
170
|
-
title?: string;
|
|
171
|
-
summary?: string;
|
|
172
|
-
path?: string;
|
|
173
|
-
imagePath?: string;
|
|
174
|
-
imageDisplayPath?: string;
|
|
175
|
-
imageMimeType?: string;
|
|
176
|
-
linesAdded?: number;
|
|
177
|
-
linesRemoved?: number;
|
|
178
|
-
linesCreated?: number;
|
|
179
|
-
fileSize?: number;
|
|
180
|
-
fileContentAfterWrite?: string;
|
|
181
|
-
diffString?: string;
|
|
182
|
-
diff?: string;
|
|
183
|
-
firstChangedLine?: number;
|
|
184
|
-
expandedText?: string;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function asCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
|
|
188
|
-
return value && typeof value === "object" ? (value as CursorReplayToolDetails) : undefined;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function inferImageMimeTypeFromPath(path: string | undefined): string | undefined {
|
|
192
|
-
switch (extname(path ?? "").toLowerCase()) {
|
|
193
|
-
case ".png":
|
|
194
|
-
return "image/png";
|
|
195
|
-
case ".jpg":
|
|
196
|
-
case ".jpeg":
|
|
197
|
-
return "image/jpeg";
|
|
198
|
-
case ".gif":
|
|
199
|
-
return "image/gif";
|
|
200
|
-
case ".webp":
|
|
201
|
-
return "image/webp";
|
|
202
|
-
default:
|
|
203
|
-
return undefined;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function readImageFileForReplay(path: string | undefined): string | undefined {
|
|
208
|
-
if (!path) return undefined;
|
|
209
|
-
try {
|
|
210
|
-
const stat = statSync(path);
|
|
211
|
-
if (!stat.isFile() || stat.size <= 0 || stat.size > 25 * 1024 * 1024) return undefined;
|
|
212
|
-
return readFileSync(path).toString("base64");
|
|
213
|
-
} catch {
|
|
214
|
-
return undefined;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function buildImageReplayComponent(text: string, imageData: string, mimeType: string, filename: string, theme: CursorReplayRenderTheme): Component {
|
|
219
|
-
const textComponent = new Text(text, 0, 0);
|
|
220
|
-
const imageComponent = new Image(imageData, mimeType, { fallbackColor: (value) => theme.fg("muted", value) }, { filename, maxWidthCells: 40, maxHeightCells: 16 });
|
|
221
|
-
return {
|
|
222
|
-
render(width: number): string[] {
|
|
223
|
-
return [...textComponent.render(width), ...imageComponent.render(width)];
|
|
224
|
-
},
|
|
225
|
-
invalidate(): void {
|
|
226
|
-
textComponent.invalidate();
|
|
227
|
-
imageComponent.invalidate();
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function getCursorReplayToolLabel(toolName: CursorReplayToolName): string {
|
|
233
|
-
if (toolName === "cursor_edit") return "edit";
|
|
234
|
-
if (toolName === "cursor_write") return "write";
|
|
235
|
-
return getCursorReplayDisplayLabel(toolName);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function getCursorReplayPath(args: Record<string, unknown> | undefined, details: CursorReplayToolDetails | undefined): string {
|
|
239
|
-
const argPath = args?.path;
|
|
240
|
-
return details?.path ?? (typeof argPath === "string" && argPath.trim() ? argPath : "unknown");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
type CursorReplayRenderCall = NonNullable<ToolDefinition<typeof cursorReplayToolSchema, unknown>["renderCall"]>;
|
|
244
|
-
type CursorReplayRenderResult = NonNullable<ToolDefinition<typeof cursorReplayToolSchema, unknown>["renderResult"]>;
|
|
245
|
-
type CursorReplayRenderTheme = Parameters<CursorReplayRenderCall>[1];
|
|
246
|
-
|
|
247
|
-
function parseUnifiedDiffHunkHeader(line: string): { oldLine: number; newLine: number } | undefined {
|
|
248
|
-
const match = /^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/.exec(line);
|
|
249
|
-
if (!match) return undefined;
|
|
250
|
-
return { oldLine: Number(match[1]), newLine: Number(match[2]) };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function replaceCursorReplayTabs(text: string): string {
|
|
254
|
-
return text.replace(/\t/g, " ");
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function formatCursorReplayDiffLine(prefix: string, lineNumber: number, content: string, theme: CursorReplayRenderTheme): string {
|
|
258
|
-
const rendered = `${prefix}${lineNumber} ${replaceCursorReplayTabs(content)}`;
|
|
259
|
-
if (prefix === "+") return theme.fg("toolDiffAdded", rendered);
|
|
260
|
-
if (prefix === "-") return theme.fg("toolDiffRemoved", rendered);
|
|
261
|
-
return theme.fg("toolDiffContext", rendered);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function formatCursorReplayDiff(diff: string, theme: CursorReplayRenderTheme, maxLines: number): string {
|
|
265
|
-
const lines = diff.split("\n");
|
|
266
|
-
const oldFileIsNull = lines.some((line) => line === "--- /dev/null");
|
|
267
|
-
const newFileIsNull = lines.some((line) => line === "+++ /dev/null");
|
|
268
|
-
const rendered: string[] = [];
|
|
269
|
-
let oldLine = 1;
|
|
270
|
-
let newLine = 1;
|
|
271
|
-
|
|
272
|
-
for (const line of lines) {
|
|
273
|
-
if (!line || line.startsWith("--- ") || line.startsWith("+++ ")) continue;
|
|
274
|
-
const hunk = parseUnifiedDiffHunkHeader(line);
|
|
275
|
-
if (hunk) {
|
|
276
|
-
oldLine = hunk.oldLine;
|
|
277
|
-
newLine = hunk.newLine;
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (line.startsWith("+")) {
|
|
282
|
-
if (newFileIsNull) continue;
|
|
283
|
-
rendered.push(formatCursorReplayDiffLine("+", newLine, line.slice(1), theme));
|
|
284
|
-
newLine += 1;
|
|
285
|
-
} else if (line.startsWith("-")) {
|
|
286
|
-
if (oldFileIsNull && line === "-") continue;
|
|
287
|
-
rendered.push(formatCursorReplayDiffLine("-", oldLine, line.slice(1), theme));
|
|
288
|
-
oldLine += 1;
|
|
289
|
-
} else if (line.startsWith(" ")) {
|
|
290
|
-
rendered.push(formatCursorReplayDiffLine(" ", newLine, line.slice(1), theme));
|
|
291
|
-
oldLine += 1;
|
|
292
|
-
newLine += 1;
|
|
293
|
-
} else {
|
|
294
|
-
rendered.push(theme.fg("toolDiffContext", replaceCursorReplayTabs(line)));
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const visible = rendered.slice(0, maxLines);
|
|
299
|
-
if (rendered.length > maxLines) visible.push(theme.fg("muted", `... (${rendered.length - maxLines} more diff lines; expand for full diff)`));
|
|
300
|
-
return visible.join("\n");
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function stripCursorReplayHeader(text: string): string {
|
|
304
|
-
const lines = text.trimEnd().split("\n");
|
|
305
|
-
return lines.length > 2 && lines[1]?.trim() === "" ? lines.slice(2).join("\n") : lines.join("\n");
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function formatMutedBlock(text: string, theme: CursorReplayRenderTheme): string {
|
|
309
|
-
return text.split("\n").map((line) => theme.fg("muted", line)).join("\n");
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function formatCursorReplayPreview(
|
|
313
|
-
text: string,
|
|
314
|
-
theme: CursorReplayRenderTheme,
|
|
315
|
-
maxLines = CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES,
|
|
316
|
-
stripHeader = true,
|
|
317
|
-
): string | undefined {
|
|
318
|
-
const body = (stripHeader ? stripCursorReplayHeader(text) : text).trimEnd();
|
|
319
|
-
if (!body) return undefined;
|
|
320
|
-
const lines = body.split("\n");
|
|
321
|
-
const visible = lines.slice(0, maxLines);
|
|
322
|
-
if (lines.length > maxLines) visible.push(`... (${lines.length - maxLines} more lines; expand for full details)`);
|
|
323
|
-
return formatMutedBlock(visible.join("\n"), theme);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function safeHighlightCursorReplayCode(text: string, path: string | undefined): string[] | undefined {
|
|
327
|
-
const lang = path ? getLanguageFromPath(path) : undefined;
|
|
328
|
-
if (!lang) return undefined;
|
|
329
|
-
try {
|
|
330
|
-
return highlightCode(replaceCursorReplayTabs(text), lang);
|
|
331
|
-
} catch {
|
|
332
|
-
return undefined;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function formatCursorReplayFilePreview(
|
|
337
|
-
text: string,
|
|
338
|
-
path: string | undefined,
|
|
339
|
-
theme: CursorReplayRenderTheme,
|
|
340
|
-
maxLines = CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES,
|
|
341
|
-
stripHeader = true,
|
|
342
|
-
): string | undefined {
|
|
343
|
-
const body = (stripHeader ? stripCursorReplayHeader(text) : text).trimEnd();
|
|
344
|
-
if (!body) return undefined;
|
|
345
|
-
const rawLines = body.split("\n");
|
|
346
|
-
const highlightedLines = safeHighlightCursorReplayCode(body, path);
|
|
347
|
-
const renderedLines = highlightedLines ?? rawLines.map((line) => theme.fg("toolOutput", replaceCursorReplayTabs(line)));
|
|
348
|
-
const visible = renderedLines.slice(0, maxLines);
|
|
349
|
-
if (rawLines.length > maxLines) visible.push(theme.fg("muted", `... (${rawLines.length - maxLines} more lines; expand for full details)`));
|
|
350
|
-
return visible.join("\n");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function getCursorReplayActivityTitle(toolName: CursorReplayToolName, args: Record<string, unknown> | undefined): string {
|
|
354
|
-
if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME && typeof args?.activityTitle === "string" && args.activityTitle.trim()) {
|
|
355
|
-
return args.activityTitle.trim();
|
|
356
|
-
}
|
|
357
|
-
return getCursorReplayToolLabel(toolName);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function getCursorReplayCallSummary(toolName: CursorReplayToolName, args: Record<string, unknown> | undefined): string | undefined {
|
|
361
|
-
const activitySummary = typeof args?.activitySummary === "string" && args.activitySummary.trim() ? args.activitySummary.trim() : undefined;
|
|
362
|
-
if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME && activitySummary) return activitySummary;
|
|
363
|
-
|
|
364
|
-
const path = typeof args?.path === "string" ? args.path : undefined;
|
|
365
|
-
const description = typeof args?.description === "string" ? args.description : undefined;
|
|
366
|
-
const prompt = typeof args?.prompt === "string" ? args.prompt : undefined;
|
|
367
|
-
const totalCount = typeof args?.totalCount === "number" ? args.totalCount : undefined;
|
|
368
|
-
const diagnosticCount = typeof args?.diagnosticCount === "number" ? args.diagnosticCount : undefined;
|
|
369
|
-
const paths = Array.isArray(args?.paths) ? args.paths.filter((entry): entry is string => typeof entry === "string") : [];
|
|
370
|
-
|
|
371
|
-
if (toolName === "cursor_edit" || toolName === "cursor_write" || toolName === "cursor_delete") return path ?? "unknown";
|
|
372
|
-
if (toolName === "cursor_read_lints") {
|
|
373
|
-
const target = paths.length > 0 ? paths.join(" ") : path;
|
|
374
|
-
if (target && diagnosticCount !== undefined) return `${target} · ${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"}`;
|
|
375
|
-
return target;
|
|
376
|
-
}
|
|
377
|
-
if (toolName === "cursor_update_todos" || toolName === "cursor_create_plan") {
|
|
378
|
-
return totalCount !== undefined ? `${totalCount} item${totalCount === 1 ? "" : "s"}` : undefined;
|
|
379
|
-
}
|
|
380
|
-
if (toolName === "cursor_task") return description;
|
|
381
|
-
if (toolName === "cursor_generate_image") return prompt;
|
|
382
|
-
if (toolName === "cursor_mcp") return typeof args?.toolName === "string" ? args.toolName : undefined;
|
|
383
|
-
if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) {
|
|
384
|
-
if (typeof args?.path === "string") return args.path;
|
|
385
|
-
if (typeof args?.toolName === "string") return args.toolName;
|
|
386
|
-
}
|
|
387
|
-
return undefined;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function renderCursorReplayCall(
|
|
391
|
-
toolName: CursorReplayToolName,
|
|
392
|
-
args: Record<string, unknown> | undefined,
|
|
393
|
-
theme: CursorReplayRenderTheme,
|
|
394
|
-
isPartial: boolean,
|
|
395
|
-
): Text {
|
|
396
|
-
if (!isPartial) return new Text("", 0, 0);
|
|
397
|
-
let text = theme.fg("toolTitle", theme.bold(`${getCursorReplayActivityTitle(toolName, args)} `));
|
|
398
|
-
const summary = getCursorReplayCallSummary(toolName, args);
|
|
399
|
-
if (summary) text += theme.fg("accent", summary);
|
|
400
|
-
return new Text(text.trimEnd(), 0, 0);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function countDisplayLines(text: string): number {
|
|
404
|
-
const withoutFinalNewline = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
405
|
-
return withoutFinalNewline ? withoutFinalNewline.split("\n").length : 0;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function renderNativeLookingCursorFileMutationCall(
|
|
409
|
-
toolName: "edit" | "write",
|
|
410
|
-
args: Record<string, unknown> | undefined,
|
|
411
|
-
theme: CursorReplayRenderTheme,
|
|
412
|
-
isPartial: boolean,
|
|
413
|
-
): Text {
|
|
414
|
-
if (!isPartial) return new Text("", 0, 0);
|
|
415
|
-
let text = theme.fg("toolTitle", theme.bold(`${toolName} `));
|
|
416
|
-
const path = typeof args?.path === "string" && args.path.trim() ? args.path : "unknown";
|
|
417
|
-
text += theme.fg("accent", path);
|
|
418
|
-
if (toolName === "write" && typeof args?.content === "string" && args.content.length > 0) {
|
|
419
|
-
const lineCount = countDisplayLines(args.content);
|
|
420
|
-
text += theme.fg("dim", ` (${pluralize(lineCount, "line")})`);
|
|
421
|
-
}
|
|
422
|
-
return new Text(text.trimEnd(), 0, 0);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function pluralize(count: number, noun: string): string {
|
|
426
|
-
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function getCursorEditDiff(details: CursorReplayToolDetails): string | undefined {
|
|
430
|
-
return details.diffString ?? details.diff;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function hasCursorEditChanges(details: CursorReplayToolDetails): boolean {
|
|
434
|
-
return Boolean(getCursorEditDiff(details)) || Boolean(details.linesAdded) || Boolean(details.linesRemoved);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function classifyCursorEditOperation(details: CursorReplayToolDetails): "created" | "deleted" | "updated" | "unchanged" {
|
|
438
|
-
if (!hasCursorEditChanges(details)) return "unchanged";
|
|
439
|
-
const diff = getCursorEditDiff(details);
|
|
440
|
-
if (diff?.startsWith("--- /dev/null")) return "created";
|
|
441
|
-
if (diff?.includes("\n+++ /dev/null")) return "deleted";
|
|
442
|
-
return "updated";
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function formatCursorEditSummary(details: CursorReplayToolDetails): string {
|
|
446
|
-
const operation = classifyCursorEditOperation(details);
|
|
447
|
-
if (operation === "unchanged") return "no changes needed";
|
|
448
|
-
if (operation === "created" && details.linesAdded !== undefined) return `created ${pluralize(details.linesAdded, "line")}`;
|
|
449
|
-
if (operation === "deleted" && details.linesRemoved !== undefined) return `deleted ${pluralize(details.linesRemoved, "line")}`;
|
|
450
|
-
const parts = [
|
|
451
|
-
details.linesAdded ? `added ${pluralize(details.linesAdded, "line")}` : undefined,
|
|
452
|
-
details.linesRemoved ? `removed ${pluralize(details.linesRemoved, "line")}` : undefined,
|
|
453
|
-
].filter((part): part is string => Boolean(part));
|
|
454
|
-
return parts.length > 0 ? parts.join(", ") : "updated file";
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function firstContentText(result: Parameters<CursorReplayRenderResult>[0]): string {
|
|
458
|
-
const content = result.content[0];
|
|
459
|
-
return content?.type === "text" ? content.text : "";
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function renderExpandableCursorReplayResult(
|
|
463
|
-
title: string,
|
|
464
|
-
result: Parameters<CursorReplayRenderResult>[0],
|
|
465
|
-
options: Parameters<CursorReplayRenderResult>[1],
|
|
466
|
-
theme: Parameters<CursorReplayRenderResult>[2],
|
|
467
|
-
context: Parameters<CursorReplayRenderResult>[3],
|
|
468
|
-
isError: boolean,
|
|
469
|
-
): Component {
|
|
470
|
-
const details = asCursorReplayToolDetails(result.details);
|
|
471
|
-
const text = firstContentText(result);
|
|
472
|
-
const summary = details?.summary ?? text.split("\n").find((line) => line.trim()) ?? "completed";
|
|
473
|
-
let rendered = `${theme.fg("toolTitle", theme.bold(title))} ${theme.fg(isError ? "error" : "success", summary)}`;
|
|
474
|
-
const expandedText = details?.expandedText ?? (text.includes("\n") ? text : undefined);
|
|
475
|
-
if (expandedText) {
|
|
476
|
-
const preview = options.expanded ? formatMutedBlock(expandedText, theme) : formatCursorReplayPreview(expandedText, theme);
|
|
477
|
-
if (preview) rendered += `\n${preview}`;
|
|
478
|
-
}
|
|
479
|
-
if (details?.cursorToolName === "generateImage" && !isError && context.showImages) {
|
|
480
|
-
const imageData = readImageFileForReplay(details.imagePath);
|
|
481
|
-
const mimeType = details.imageMimeType ?? inferImageMimeTypeFromPath(details.imagePath);
|
|
482
|
-
if (imageData && mimeType) return buildImageReplayComponent(rendered, imageData, mimeType, basename(details.imagePath ?? "generated-image"), theme);
|
|
483
|
-
}
|
|
484
|
-
return new Text(rendered, 0, 0);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function renderCursorGenerateImageResult(
|
|
488
|
-
result: Parameters<CursorReplayRenderResult>[0],
|
|
489
|
-
options: Parameters<CursorReplayRenderResult>[1],
|
|
490
|
-
theme: Parameters<CursorReplayRenderResult>[2],
|
|
491
|
-
context: Parameters<CursorReplayRenderResult>[3],
|
|
492
|
-
isError: boolean,
|
|
493
|
-
): Component {
|
|
494
|
-
return renderExpandableCursorReplayResult("Cursor generateImage", result, options, theme, context, isError);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function renderCursorReplayResult(
|
|
498
|
-
result: Parameters<CursorReplayRenderResult>[0],
|
|
499
|
-
options: Parameters<CursorReplayRenderResult>[1],
|
|
500
|
-
theme: Parameters<CursorReplayRenderResult>[2],
|
|
501
|
-
context: Parameters<CursorReplayRenderResult>[3],
|
|
502
|
-
isError: boolean,
|
|
503
|
-
): Component {
|
|
504
|
-
if (options.isPartial) return new Text(theme.fg("warning", "Replaying Cursor tool result..."), 0, 0);
|
|
505
|
-
const details = asCursorReplayToolDetails(result.details);
|
|
506
|
-
const text = firstContentText(result);
|
|
507
|
-
if (isError && !details?.title) return new Text(theme.fg("error", text.split("\n")[0] || "Cursor replay failed"), 0, 0);
|
|
508
|
-
|
|
509
|
-
if (details?.cursorToolName === "edit" && hasCursorEditChanges(details)) {
|
|
510
|
-
const summary = formatCursorEditSummary(details);
|
|
511
|
-
const title = details.title ?? "edit";
|
|
512
|
-
let rendered = `${theme.fg("toolTitle", theme.bold(title))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
|
|
513
|
-
const diff = getCursorEditDiff(details);
|
|
514
|
-
if (diff) rendered += `\n${formatCursorReplayDiff(diff, theme, options.expanded ? 40 : CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES)}`;
|
|
515
|
-
return new Text(rendered, 0, 0);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (details?.cursorToolName === "write") {
|
|
519
|
-
const parts = [
|
|
520
|
-
details.linesCreated !== undefined ? `${details.linesCreated} line${details.linesCreated === 1 ? "" : "s"}` : undefined,
|
|
521
|
-
details.fileSize !== undefined ? `${details.fileSize} bytes` : undefined,
|
|
522
|
-
].filter(Boolean);
|
|
523
|
-
const summary = parts.length > 0 ? parts.join(", ") : "written";
|
|
524
|
-
let rendered = `${theme.fg("toolTitle", theme.bold("write"))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
|
|
525
|
-
const previewSource = details.fileContentAfterWrite ?? details.expandedText ?? text;
|
|
526
|
-
const preview = formatCursorReplayFilePreview(
|
|
527
|
-
previewSource,
|
|
528
|
-
getCursorReplayPath(undefined, details),
|
|
529
|
-
theme,
|
|
530
|
-
CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES,
|
|
531
|
-
details.fileContentAfterWrite === undefined,
|
|
532
|
-
);
|
|
533
|
-
if (preview) rendered += `\n${preview}`;
|
|
534
|
-
return new Text(rendered, 0, 0);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (details?.cursorToolName === "generateImage") return renderCursorGenerateImageResult(result, options, theme, context, isError);
|
|
538
|
-
if (details?.title) return renderExpandableCursorReplayResult(details.title, result, options, theme, context, isError);
|
|
539
|
-
return new Text(text || theme.fg("success", "Cursor tool result replayed"), 0, 0);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function createCursorReplayOnlyToolDefinition(toolName: CursorReplayToolName): ToolDefinition<typeof cursorReplayToolSchema, unknown> {
|
|
543
|
-
const cursorToolName = toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "activity" : getCursorReplaySourceToolName(toolName);
|
|
544
|
-
const sideEffectDescription = toolName === "cursor_edit" || toolName === "cursor_write" || toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "file mutations" : "real tool work";
|
|
545
|
-
return {
|
|
546
|
-
name: toolName,
|
|
547
|
-
label: getCursorReplayToolLabel(toolName),
|
|
548
|
-
description: `Replay display for a Cursor SDK ${cursorToolName} operation. This tool only returns recorded Cursor results and never executes ${sideEffectDescription} directly.`,
|
|
549
|
-
promptSnippet: `Render a recorded Cursor SDK ${cursorToolName} operation without executing ${sideEffectDescription}.`,
|
|
550
|
-
promptGuidelines: [
|
|
551
|
-
`Use this tool only for replaying Cursor SDK ${cursorToolName} results that were already produced by Cursor; it does not execute ${sideEffectDescription}.`,
|
|
552
|
-
],
|
|
553
|
-
parameters: cursorReplayToolSchema,
|
|
554
|
-
async execute() {
|
|
555
|
-
throw new Error(`No recorded Cursor ${cursorToolName} result was available. This replay-only tool does not execute ${sideEffectDescription}.`);
|
|
556
|
-
},
|
|
557
|
-
renderCall(args, theme, context) {
|
|
558
|
-
return renderCursorReplayCall(toolName, args as Record<string, unknown>, theme, context.isPartial);
|
|
559
|
-
},
|
|
560
|
-
renderResult(result, options, theme, context) {
|
|
561
|
-
return renderCursorReplayResult(result, options, theme, context, context.isError);
|
|
562
|
-
},
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function createNativeCursorToolDefinition(toolName: NativeCursorToolName, cwd: string): ToolDefinition<TSchema, unknown, unknown> {
|
|
567
|
-
if (toolName === "read") return createReadToolDefinition(cwd) as ToolDefinition<TSchema, unknown, unknown>;
|
|
568
|
-
if (toolName === "bash") return createBashToolDefinition(cwd) as ToolDefinition<TSchema, unknown, unknown>;
|
|
569
|
-
if (toolName === "edit") return createEditToolDefinition(cwd) as ToolDefinition<TSchema, unknown, unknown>;
|
|
570
|
-
if (toolName === "write") return createWriteToolDefinition(cwd) as ToolDefinition<TSchema, unknown, unknown>;
|
|
571
|
-
if (toolName === "grep") return createGrepToolDefinition(cwd) as ToolDefinition<TSchema, unknown, unknown>;
|
|
572
|
-
if (toolName === "find") return createFindToolDefinition(cwd) as ToolDefinition<TSchema, unknown, unknown>;
|
|
573
|
-
if (toolName === "ls") return createLsToolDefinition(cwd) as ToolDefinition<TSchema, unknown, unknown>;
|
|
574
|
-
if (isCursorReplayToolName(toolName)) return createCursorReplayOnlyToolDefinition(toolName) as ToolDefinition<TSchema, unknown, unknown>;
|
|
575
|
-
throw new Error(`Unsupported Cursor native replay tool: ${toolName}`);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function registerNativeCursorTool(pi: Pick<ExtensionAPI, "registerTool">, toolName: NativeCursorToolName): void {
|
|
579
|
-
const definition = createNativeCursorToolDefinition(toolName, getCursorSessionCwd());
|
|
580
|
-
pi.registerTool(wrapNativeCursorTool(definition, () => createNativeCursorToolDefinition(toolName, getCursorSessionCwd())));
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function hasNonBuiltinTool(pi: Pick<ExtensionAPI, "getAllTools">, toolName: NativeCursorToolName): boolean {
|
|
584
|
-
const existingTool = pi.getAllTools().find((tool) => tool.name === toolName);
|
|
585
|
-
return existingTool !== undefined && existingTool.sourceInfo.source !== "builtin";
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
type NativeRegistrationContext = { hasUI: boolean; ui: Pick<ExtensionContext["ui"], "notify">; model?: ExtensionContext["model"] };
|
|
589
|
-
|
|
590
|
-
function isCursorModel(model: ExtensionContext["model"]): boolean {
|
|
591
|
-
return model?.provider === "cursor" || model?.api === "cursor-sdk";
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function syncRegisteredNativeCursorToolsForModel(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">, model: ExtensionContext["model"]): void {
|
|
595
|
-
if (registeredNativeToolNames.size === 0) return;
|
|
596
|
-
const activeToolNames = new Set(pi.getActiveTools());
|
|
597
|
-
let changed = false;
|
|
598
|
-
if (isCursorModel(model)) {
|
|
599
|
-
for (const toolName of registeredNativeToolNames) {
|
|
600
|
-
if (isCursorReplayToolName(toolName) && !CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES.some((activeReplayToolName) => activeReplayToolName === toolName)) continue;
|
|
601
|
-
if (activeToolNames.has(toolName)) continue;
|
|
602
|
-
activeToolNames.add(toolName);
|
|
603
|
-
changed = true;
|
|
604
|
-
}
|
|
605
|
-
} else {
|
|
606
|
-
for (const toolName of CURSOR_REPLAY_TOOL_NAMES) {
|
|
607
|
-
if (!activeToolNames.delete(toolName)) continue;
|
|
608
|
-
changed = true;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
if (changed) pi.setActiveTools([...activeToolNames]);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function registerAvailableNativeCursorTools(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
|
|
615
|
-
if (!isCursorNativeToolRegistrationRequested()) {
|
|
616
|
-
registeredNativeToolNames.clear();
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const skippedToolNames: string[] = [];
|
|
621
|
-
for (const toolName of NATIVE_CURSOR_TOOL_NAMES) {
|
|
622
|
-
if (registeredNativeToolNames.has(toolName)) continue;
|
|
623
|
-
if (hasNonBuiltinTool(pi, toolName)) {
|
|
624
|
-
skippedToolNames.push(toolName);
|
|
625
|
-
continue;
|
|
626
|
-
}
|
|
627
|
-
registerNativeCursorTool(pi, toolName);
|
|
628
|
-
registeredNativeToolNames.add(toolName);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
syncRegisteredNativeCursorToolsForModel(pi, ctx.model);
|
|
632
|
-
|
|
633
|
-
if (skippedToolNames.length > 0 && readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) === true && ctx.hasUI) {
|
|
634
|
-
ctx.ui.notify(
|
|
635
|
-
`Cursor native tool replay skipped for ${skippedToolNames.join(", ")} because another extension already provides ${skippedToolNames.length === 1 ? "that tool" : "those tools"}. Cursor will use scrubbed activity transcripts for skipped tools.`,
|
|
636
|
-
"warning",
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
export function registerCursorNativeToolDisplay(pi: CursorNativeToolDisplayExtensionApi): void {
|
|
642
|
-
pi.on("session_start", (_event, ctx) => {
|
|
643
|
-
registerAvailableNativeCursorTools(pi, ctx);
|
|
644
|
-
});
|
|
645
|
-
pi.on("model_select", (event) => {
|
|
646
|
-
syncRegisteredNativeCursorToolsForModel(pi, event.model);
|
|
647
|
-
});
|
|
648
|
-
}
|
|
1
|
+
export type { CursorNativeToolDisplayItem } from "./cursor-native-tool-display-state.js";
|
|
2
|
+
export {
|
|
3
|
+
canRenderCursorToolNatively,
|
|
4
|
+
deleteCursorNativeToolDisplay,
|
|
5
|
+
isCursorNativeToolDisplayEnabled,
|
|
6
|
+
isCursorNativeToolDisplayRuntimeEnabled,
|
|
7
|
+
recordCursorNativeToolDisplay,
|
|
8
|
+
__testUtils,
|
|
9
|
+
} from "./cursor-native-tool-display-state.js";
|
|
10
|
+
export { registerCursorNativeToolDisplay } from "./cursor-native-tool-display-registration.js";
|