pi-cursor-sdk 0.1.35 → 0.1.37

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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.37 - 2026-06-06
6
+
7
+ ### Changed
8
+
9
+ - Cut Cursor native replay over to one canonical replay surface: neutral `cursor` activity cards plus native-compatible `read`, `bash`, `grep`, `find`, `ls`, `edit`, and `write` cards. Legacy `cursor_*` replay wrapper names, alias exports, old `cursorToolName` replay-detail parsing, and replay-only prompt metadata are removed instead of preserved behind compatibility shims (#123).
10
+ - Keep edit/write replay previews on the shared structured diff/file preview renderers, while retaining unstructured `expandedText` unified-diff extraction only as a fallback for current SDK payloads that do not include structured diff fields.
11
+
12
+ ### Fixed
13
+
14
+ - Stop registering duplicate replay-only prompt snippets/guidelines for every Cursor SDK activity wrapper, eliminating the inflated prompt metadata reported by issue #123 while preserving current TUI replay card titles, summaries, typed details, and `sourceToolName` display metadata.
15
+
16
+ ## 0.1.36 - 2026-06-05
17
+
18
+ ### Fixed
19
+
20
+ - Classify Cursor backend `ConnectError: [unavailable] Error` failures with code 14 and `aiserver.v1.ErrorDetails` as recoverable network/service errors, preventing duplicate process-level uncaught exceptions from crashing pi while still surfacing scrubbed retry guidance.
21
+
5
22
  ## 0.1.35 - 2026-06-05
6
23
 
7
24
  ### Changed
@@ -30,7 +30,7 @@ Current implementation notes:
30
30
  - Bridge diagnostics are opt-in only: `PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1` writes typed, allowlisted, scrubbed single-line JSONL records to `process.stderr` with prefix `[pi-cursor-sdk:bridge]`. Diagnostics are scrubbed operational logs, not anonymous telemetry. They intentionally include tool names, safe correlation IDs, run lifecycle, exposed pi↔MCP name pairs, queued requests, result resolution, rejection, cancellation, and pending counts. Correlation IDs are generated independently from the tokenized endpoint path, and Cursor MCP call IDs are hashed before serialization. Diagnostics must not include endpoint paths/URLs/path components/tokens, API keys, bearer tokens, cookies, session credentials, raw args/results, stdout/stderr payloads, file contents, Cursor settings output, or local private session paths in tracked docs, and they must not call pi UI status, notification, or footer APIs. If tool names themselves are unacceptable for a release target, bridge debug diagnostics are not safe for shared logs under the current contract.
31
31
  - This repo does not provide a generic desktop-automation, browser-driver, or CDP recipe. Provider docs should describe pi-cursor-sdk's Cursor provider/bridge contract only.
32
32
  - Cursor internal tool activity is recorded from SDK events and scrubbed. Maintainer reference for all 16 `@cursor/sdk@1.0.17` `ToolType` values, runtime alias normalization, and intentional mapping/fallback rules: [Cursor native tool replay — SDK ToolType replay matrix](./cursor-native-tool-replay.md#sdk-tooltype-replay-matrix) (official SDK docs: https://cursor.com/docs/sdk/typescript). In interactive TTY sessions, supported completed `read`, `bash`, `grep`, `find`, `ls`, `edit`, `write`, diagnostics, delete, todo/plan, task, image generation, MCP, semantic search, and screen recording activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so the TUI can show native-looking cards without rerunning Cursor's reads/shell commands/file edits. Cursor `glob` activity is replayed through native `find` cards. Cursor write activity is replayed through native-looking `write` cards, and Cursor StrReplace/edit activity uses native-looking `edit` only when recorded arguments truthfully satisfy pi's `edit` schema; path-only Cursor edit and notebook edit replay falls back to neutral Cursor activity before pi validation. Diagnostics, delete, todos/plans, task, image, and MCP activity use neutral Cursor activity cards with pi's default success/error shell. Neutral Cursor activity calls include `activityTitle` and, when available, `activitySummary` so partial/collapsed cards preserve identity such as `Cursor plan`, `Cursor todos`, `Cursor MCP`, or `Cursor edit`. For long-running or externally meaningful Cursor tools (`task`, `shell`, `mcp`, `generateImage`, `recordScreen`, `semSearch`, web search/fetch, plan/todo), the provider may surface one low-noise deferred in-progress thinking line such as `Cursor MCP: external_search` from bounded, scrubbed SDK args; fast local tools (`read`, `grep`, `glob`, and similar) skip lifecycle lines when completion follows immediately, and pi bridge MCP calls are excluded because pi already shows real pi tool execution ([lifecycle visibility](./cursor-native-tool-replay.md#low-noise-tool-lifecycle-visibility)). Replay-only tools display recorded Cursor results, normalize workspace-local paths/diff headers for display, use pi diff colors for edit previews and path-inferred syntax highlighting for write previews, and fail closed if called without a recorded result. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. Cursor workflow tools such as mode/task/todo/plan activity are not pi workflow controls; reported todo/plan events are displayed as Cursor activity only. Plan/todo replay cards can be followed by Cursor's final plan text, selected from `run.wait().result` when Cursor provides one and trimmed against already-emitted text. Started Cursor SDK tool calls that never receive a completion event are surfaced with bounded user-visible labels/traces (neutral activity cards when native replay routing allows, otherwise the same inactive or transcript trace fallbacks used for completed replay) instead of being silently discarded when the run failed/aborted, produced no assistant text, or involved external/side-effectful tools; incomplete fast local discovery starts (`read`, `grep`, `glob`, `ls`) remain maintainer-debug-only after successful text-producing runs so stale SDK start events do not create red post-answer cards. Explicit failures remain visible when Cursor reports them through completed tool calls or step results. Pi bridge MCP starts remain excluded from duplicate incomplete Cursor cards because pi already shows real pi tool execution. `PI_CURSOR_NATIVE_TOOL_DISPLAY=0` disables native replay, and `PI_CURSOR_REGISTER_NATIVE_TOOLS=0` is a registration-only opt-out that keeps the transcript fallback without shadowing pi tool names. When bridge or native replay cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK activity arrives: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later tool batches as further `toolUse` turns, then Cursor's final assistant answer. For shell replay, completed `stdout` / `stderr` are primary; unambiguous `shell-output-delta` data is used only as display-only fallback for empty successful shell completions, and overlapping shell calls drop ambiguous deltas instead of guessing. Non-interactive runs keep bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when no live-run turn split is active.
33
- - Synthetic replay names are internal compatibility details. New model-facing prompt text and user-visible cards use native tool names when renderer-compatible, or neutral Cursor activity labels when not. Legacy sessions containing old internal replay names are sanitized before prompt/display. Bridge MCP names such as `pi__sem_reindex` are MCP-only; pi session output uses real pi tool names.
33
+ - Cursor native replay uses one neutral replay tool name, `cursor`, plus native-compatible card names when renderer-compatible (`read`, `bash`, `grep`, `find`, `ls`, `edit`, `write`). Neutral replay identity lives in `activityTitle`, `activitySummary`, and typed replay details, not in extra registered tool names. Bridge MCP names such as `pi__sem_reindex` are MCP-only; pi session output uses real pi tool names.
34
34
  - Cursor SDK usage events report cumulative internal agent/tool/cache work, not the replayable pi prompt context. The extension does not copy raw Cursor SDK usage into pi usage or compaction. For Cursor assistant messages, `usage.input`/`usage.output` are approximate pi session activity components: initial Cursor prompt input is counted once, consumed split-run tool results are counted as deduped input on the following assistant turn, and assistant output includes visible text/thinking/tool-call content. `usage.totalTokens` is the replayable Cursor prompt/context estimate derived from the same `buildCursorPrompt()` path used for `Agent.send`; it may differ from `input + output` and is the context-safe value for display/compaction. `src/cursor-usage-accounting.ts` owns this usage policy, and `src/cursor-live-run-accounting.ts` owns prompt-once and consumed-tool-result accounting so provider usage and bridge result resolution share the same matched tool-result boundary.
35
35
  - Audit observation, 2026-05-19, superseded by the 2026-05-21 replay pass and #68 incomplete visibility, then narrowed by the 2026-05-26 fast-local suppression: a missing-file read with Composer 2.5 emitted `tool-call-started` for Cursor `read`, then streamed final text `Error: File not found`, but did not emit `tool-call-completed` or an `onStep` `toolCall` error result. Leftover external/side-effectful started calls are surfaced at run completion through the same native replay routing as completed tools (activity cards when allowed, otherwise inactive/transcript traces), while fast local discovery starts are debug-only after a successful text-producing run. Cursor-reported completed/step errors remain visible.
36
36
  - Maintainer visual verification for replay-card changes should follow [Cursor Native Tool Visual Audit Workflow](./cursor-native-tool-visual-audit.md): offscreen PTY-driven pi run, xterm.js/Playwright screenshot rendering, and JSONL inspection before accepting commits or PRs.
@@ -72,7 +72,7 @@ Source of truth for SDK tool names: `@cursor/sdk@1.0.17` conversation `ToolType`
72
72
 
73
73
  Implementation owners: `src/cursor-tool-presentation-registry.ts` (canonical names, labels, visibility, replay policy, bridge exclusions for internal replay wrappers, and display-spec key completeness), `src/cursor-transcript-tool-specs.ts` (registry-keyed `TOOL_DISPLAY_SPECS` formatters/builders), `src/cursor-native-tool-display-replay.ts` (replay card rendering derived from registry replay metadata), and `src/cursor-transcript-utils.ts` (`normalizeToolName()` delegating to the registry).
74
74
 
75
- **Maintainer invariants — edit/write replay previews:** All colored diff rendering (native `edit` cards and `Cursor edit` activity fallbacks) flows through the single `formatCursorReplayDiff()` in `src/cursor-native-tool-display-replay.ts`. Activity write fallbacks with structured `fileContentAfterWrite` use the same `formatCursorReplayFilePreview()` path as native `write` cards. Structured `diffString` (and `diff`/`lines*`) or `fileContentAfterWrite` on `CursorReplay*Details` (including activity variants) is the source of truth for TUI preview coloring/highlighting. `expandedText` on activity details is for summary/expansion and legacy JSONL compatibility only; it is never the primary preview source when structured fields are present. Legacy paths retain `extractUnifiedDiffSection` + delegation solely for old session JSONL that predates structured population; no parallel +/- coloring loops exist for new paths.
75
+ **Maintainer invariants — edit/write replay previews:** All colored diff rendering (native `edit` cards and `Cursor edit` activity fallbacks) flows through the single `formatCursorReplayDiff()` in `src/cursor-native-tool-display-replay.ts`. Activity write fallbacks with structured `fileContentAfterWrite` use the same `formatCursorReplayFilePreview()` path as native `write` cards. Structured `diffString` (and `diff`/`lines*`) or `fileContentAfterWrite` on `CursorReplay*Details` (including activity variants) is the source of truth for TUI preview coloring/highlighting. `expandedText` on activity details is for summary/expansion and as a fallback when the current SDK reports a unified diff only in text; it is never the primary preview source when structured fields are present. No parallel +/- coloring loops exist.
76
76
 
77
77
  This matrix covers **Cursor native tool replay only**. It does not describe the [live pi MCP bridge](#live-bridge-vs-replay) or Cursor-native host tools, settings, plugins, and configured MCP servers from the Cursor SDK local-agent path.
78
78
 
@@ -100,9 +100,9 @@ This matrix covers **Cursor native tool replay only**. It does not describe the
100
100
 
101
101
  **Unknown/future fallback path:** SDK tool names with no registry-backed `TOOL_DISPLAY_SPECS` entry (future or unknown types) use `buildGenericPiToolDisplay()` in `src/cursor-transcript-tool-specs.ts` with bounded `formatFallback()` content from `src/cursor-transcript-tool-formatters.ts`. Lookup uses `Object.hasOwn(TOOL_DISPLAY_SPECS, name)` so inherited object keys such as `constructor` or `toString` cannot accidentally match a registry spec. When native replay is enabled, those completions queue through neutral pi tool name `cursor` (not native pi `read`/`bash`/… cards). Collapsed labels read like **Cursor futureSemSearchWidget** (title `Cursor` plus the SDK tool name) with optional bounded `activitySummary` from scrubbed args/result lines. Errors keep `details.summary` undefined so unbounded raw errors do not leak into replay cards (#52). Known explicit specs still win over this path; real pi bridge tool names such as `edit` and `write` are not suppressed by internal replay-wrapper exclusions.
102
102
 
103
- **Replay detail disposition model:** `src/cursor-replay-tool-details.ts` stores replay card disposition separately from SDK source tool identity. Variants are `nativeEdit`, `nativeWrite`, `activity` (`sourceToolName` + display `title`), `generateImage`, and `genericFallback`. Path-only or notebook edit/write fallbacks produce `activity` details (neutral `cursor` cards) instead of structured edit/write variants with optional `title` escape hatches. Native edit/write cards use `nativeEdit` / `nativeWrite` only when pi-facing replay args satisfy the matching schema. The renderer dispatches on `variant` only; legacy payloads with `cursorToolName`/`title` are parsed into the matching disposition at the boundary.
103
+ **Replay detail disposition model:** `src/cursor-replay-tool-details.ts` stores replay card disposition separately from SDK source tool identity. Variants are `nativeEdit`, `nativeWrite`, `activity` (`sourceToolName` + display `title`), `generateImage`, and `genericFallback`. Path-only or notebook edit/write fallbacks produce `activity` details (neutral `cursor` cards) instead of structured edit/write variants with optional `title` escape hatches. Native edit/write cards use `nativeEdit` / `nativeWrite` only when pi-facing replay args satisfy the matching schema. The renderer dispatches on `variant` only.
104
104
 
105
- Neutral activity rows use pi tool name `cursor` with `activityTitle` / `activitySummary` metadata. Legacy internal replay label keys such as `cursor_sem_search` are compatibility details; user-visible collapsed cards use labels like **Cursor semantic search**.
105
+ Neutral activity rows use pi tool name `cursor` with `activityTitle` / `activitySummary` metadata. User-visible collapsed cards use labels like **Cursor semantic search**.
106
106
 
107
107
  ## Runtime alias normalization
108
108
 
@@ -184,15 +184,15 @@ For shell replay, completed `stdout` / `stderr` remain the primary source. If a
184
184
 
185
185
  Non-interactive and session consumers still receive bounded scrubbed transcript data so `pi -p` keeps printing normal assistant text.
186
186
 
187
- ## Synthetic-name policy
187
+ ## Replay-name policy
188
188
 
189
- Synthetic replay names are internal compatibility details. New model-facing prompt text and user-visible cards use native tool names when renderer-compatible, or neutral Cursor activity labels when not. Legacy sessions that already contain old internal replay names are rewritten to safe labels in prompt text and display surfaces.
189
+ Cursor native replay has one neutral replay tool name, `cursor`, plus native-compatible card names when renderer-compatible: `read`, `bash`, `grep`, `find`, `ls`, `edit`, and `write`. Neutral replay identity lives in `activityTitle`, `activitySummary`, and typed replay details, not in extra registered tool names.
190
190
 
191
191
  Bridge MCP names are also not pi tool names. Cursor may see names such as `pi__sem_reindex` inside the local MCP bridge, but pi session output uses the real pi tool name.
192
192
 
193
193
  ## Conflicts and opt out
194
194
 
195
- Native replay wrappers are registered only for tool names not already owned by another extension. If another extension already owns a wrapper name needed for replay, pi-cursor-sdk skips only the conflicting wrapper and uses the scrubbed Cursor activity transcript for that tool instead. Legacy replay wrappers remain registered for old sessions, but their model-facing and user-visible labels are sanitized.
195
+ Native replay wrappers are registered only for tool names not already owned by another extension. If another extension already owns a wrapper name needed for replay, pi-cursor-sdk skips only the conflicting wrapper and uses the scrubbed Cursor activity transcript for that tool instead.
196
196
 
197
197
  Disable native replay registration entirely:
198
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "pi provider extension backed by @cursor/sdk local agents",
5
5
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
6
6
  "license": "MIT",
@@ -103,7 +103,6 @@ export function buildIncompleteCursorToolDisplay(
103
103
  return {
104
104
  toolName: CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
105
105
  args: {
106
- cursorToolName: visibility.normalizedName,
107
106
  activityTitle,
108
107
  activitySummary: reasonText,
109
108
  incomplete: true,
@@ -8,9 +8,6 @@ import { LOCAL_READ_PREVIEW_NOTICE, isLocalReadPreviewContent } from "./cursor-t
8
8
  import {
9
9
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
10
10
  getCursorReplayCallSummary,
11
- getCursorReplaySideEffectDescription,
12
- getCursorReplayOperationLabel,
13
- getCursorReplayWrapperLabel,
14
11
  type CursorReplayToolName,
15
12
  } from "./cursor-tool-presentation-registry.js";
16
13
  import {
@@ -28,24 +25,17 @@ import {
28
25
 
29
26
  export type {
30
27
  CursorReplayNativeEditDetails,
31
- CursorReplayEditDetails,
32
28
  CursorReplayGenerateImageDetails,
33
29
  CursorReplayGenericFallbackDetails,
34
30
  CursorReplayActivityDetails,
35
- CursorReplayTitledActivityDetails,
36
31
  CursorReplayToolDetails,
37
32
  CursorReplayNativeWriteDetails,
38
- CursorReplayWriteDetails,
39
33
  } from "./cursor-replay-tool-details.js";
40
34
  export {
41
- asCursorReplayToolDetails,
42
35
  isCursorReplayNativeEditDetails,
43
- isCursorReplayEditDetails,
44
36
  isCursorReplayGenerateImageDetails,
45
37
  isCursorReplayActivityDetails,
46
- isCursorReplayTitledActivityDetails,
47
38
  isCursorReplayNativeWriteDetails,
48
- isCursorReplayWriteDetails,
49
39
  parseCursorReplayToolDetails,
50
40
  } from "./cursor-replay-tool-details.js";
51
41
 
@@ -245,10 +235,8 @@ function formatCursorReplayActivityDiffPreview(
245
235
  const body = (stripHeader ? stripCursorReplayHeader(text) : text).trimEnd();
246
236
  const diffSection = body ? extractUnifiedDiffSection(body) : undefined;
247
237
  if (!diffSection || !hasUnifiedDiffHunk(diffSection)) return undefined;
248
- // Legacy-only shim (old JSONL with no diffString on the activity details).
249
- // All actual diff coloring now lives in the single `formatCursorReplayDiff` renderer.
250
- // This path exists solely so pre-structured sessions still get colored diffs via the same
251
- // high-quality implementation used for nativeEdit and new structured activity cases.
238
+ // Fallback for unstructured activity details that carry a unified diff only in expanded text.
239
+ // All actual diff coloring lives in the single `formatCursorReplayDiff` renderer.
252
240
  return formatCursorReplayDiff(diffSection, theme, maxLines);
253
241
  }
254
242
 
@@ -350,11 +338,11 @@ export function formatCursorReplayFilePreview(
350
338
  return renderedLines.join("\n");
351
339
  }
352
340
 
353
- function getCursorReplayActivityTitle(toolName: CursorReplayToolName, args: Record<string, unknown> | undefined): string {
341
+ function getCursorReplayCardTitle(toolName: CursorReplayToolName, args: Record<string, unknown> | undefined): string {
354
342
  if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME && typeof args?.activityTitle === "string" && args.activityTitle.trim()) {
355
343
  return args.activityTitle.trim();
356
344
  }
357
- return getCursorReplayWrapperLabel(toolName);
345
+ return "Cursor activity";
358
346
  }
359
347
 
360
348
  export function renderCursorReplayCall(
@@ -364,7 +352,7 @@ export function renderCursorReplayCall(
364
352
  isPartial: boolean,
365
353
  ): Text {
366
354
  if (!isPartial) return new Text("", 0, 0);
367
- let text = theme.fg("toolTitle", theme.bold(`${getCursorReplayActivityTitle(toolName, args)} `));
355
+ let text = theme.fg("toolTitle", theme.bold(`${getCursorReplayCardTitle(toolName, args)} `));
368
356
  const summary = getCursorReplayCallSummary(toolName, args);
369
357
  if (summary) text += theme.fg("accent", summary);
370
358
  return new Text(text.trimEnd(), 0, 0);
@@ -602,19 +590,13 @@ export function renderNativeLookingCursorReadReplayResult(
602
590
  }
603
591
 
604
592
  export function createCursorReplayOnlyToolDefinition(toolName: CursorReplayToolName): ToolDefinition<typeof cursorReplayToolSchema, unknown> {
605
- const cursorToolName = toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "activity" : getCursorReplayOperationLabel(toolName);
606
- const sideEffectDescription = getCursorReplaySideEffectDescription(toolName);
607
593
  return {
608
594
  name: toolName,
609
- label: getCursorReplayWrapperLabel(toolName),
610
- description: `Replay display for a Cursor SDK ${cursorToolName} operation. This tool only returns recorded Cursor results and never executes ${sideEffectDescription} directly.`,
611
- promptSnippet: `Render a recorded Cursor SDK ${cursorToolName} operation without executing ${sideEffectDescription}.`,
612
- promptGuidelines: [
613
- `Use this tool only for replaying Cursor SDK ${cursorToolName} results that were already produced by Cursor; it does not execute ${sideEffectDescription}.`,
614
- ],
595
+ label: "Cursor activity",
596
+ description: "Display recorded Cursor SDK tool activity. This tool only returns recorded Cursor results and never executes work directly.",
615
597
  parameters: cursorReplayToolSchema,
616
598
  async execute() {
617
- throw new Error(`No recorded Cursor ${cursorToolName} result was available. This replay-only tool does not execute ${sideEffectDescription}.`);
599
+ throw new Error("No recorded Cursor activity result was available. This replay-only tool does not execute work directly.");
618
600
  },
619
601
  renderCall(args, theme, context) {
620
602
  return renderCursorReplayCall(toolName, args as Record<string, unknown>, theme, context.isPartial);
@@ -13,7 +13,6 @@ import type { TSchema } from "typebox";
13
13
  import { getCursorSessionCwd } from "./cursor-session-cwd.js";
14
14
  import {
15
15
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
16
- CURSOR_REPLAY_LEGACY_TOOL_NAMES,
17
16
  isCursorReplayToolName,
18
17
  } from "./cursor-tool-names.js";
19
18
  import {
@@ -31,7 +30,7 @@ import {
31
30
  } from "./cursor-native-tool-display-state.js";
32
31
 
33
32
  const CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME] as const;
34
- const CURSOR_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME, ...CURSOR_REPLAY_LEGACY_TOOL_NAMES] as const;
33
+ const CURSOR_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME] as const;
35
34
 
36
35
  type AnyToolDefinition = ToolDefinition<TSchema, unknown, unknown>;
37
36
  type RenderCall = NonNullable<AnyToolDefinition["renderCall"]>;
@@ -9,7 +9,7 @@ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
9
9
  const AUTH_CURSOR_SDK_ERROR_MESSAGE =
10
10
  "Cursor SDK request failed because the Cursor SDK API key may be invalid or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
11
11
  const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
12
- "Cursor SDK request timed out during network I/O. Check your connection and retry; if this keeps happening, try again later or verify Cursor service availability.";
12
+ "Cursor SDK request failed during network or service I/O. Check your connection and retry; if this keeps happening, try again later or verify Cursor service availability.";
13
13
 
14
14
  const GENERIC_CURSOR_RUN_FAILURE_TEXT = "cursor sdk run failed";
15
15
 
@@ -47,6 +47,10 @@ function isUnauthenticatedConnectCode(code: unknown): boolean {
47
47
  return code === 16 || (typeof code === "string" && /^(?:16|unauthenticated)$/i.test(code));
48
48
  }
49
49
 
50
+ function isUnavailableConnectCode(code: unknown): boolean {
51
+ return code === 14 || (typeof code === "string" && /^(?:14|unavailable)$/i.test(code));
52
+ }
53
+
50
54
  function isCursorExtensionConnectStack(stack: string): boolean {
51
55
  return stack.includes("@connectrpc/connect-node") && /(?:^|[\\/])pi-cursor-sdk(?:[\\/]|$)/.test(stack);
52
56
  }
@@ -101,6 +105,10 @@ export function classifyCursorConnectError(error: unknown): CursorConnectErrorCl
101
105
  return { kind: "unauthenticated", source: getCursorConnectSource(error, record) };
102
106
  }
103
107
 
108
+ if (isUnavailableConnectCode(code)) {
109
+ return { kind: "network", source: getCursorConnectSource(error, record) };
110
+ }
111
+
104
112
  const causeCode = getErrorStringField(cause, "code");
105
113
  const causeSyscall = getErrorStringField(cause, "syscall");
106
114
  if (isLikelyNetworkTimeout(`${message}\n${rawMessage}\n${causeCode ?? ""}\n${causeSyscall ?? ""}`)) {
@@ -179,8 +187,9 @@ export function sanitizeCursorProviderError(error: unknown, apiKey?: string): st
179
187
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
180
188
  if (message === MISSING_CURSOR_API_KEY_MESSAGE) return MISSING_CURSOR_API_KEY_MESSAGE;
181
189
  const scrubbed = scrubSensitiveText(message, apiKey).trim();
182
- if (isUnauthenticatedConnectError(error) || isLikelyAuthError(scrubbed)) return AUTH_CURSOR_SDK_ERROR_MESSAGE;
190
+ const connectClassification = classifyCursorConnectError(error);
191
+ if (connectClassification?.kind === "unauthenticated" || isLikelyAuthError(scrubbed)) return AUTH_CURSOR_SDK_ERROR_MESSAGE;
192
+ if (connectClassification?.kind === "network" || isLikelyNetworkTimeout(scrubbed)) return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
183
193
  if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
184
- if (isLikelyNetworkTimeout(scrubbed)) return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
185
194
  return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
186
195
  }
@@ -55,8 +55,6 @@ export interface CursorReplayGenerateImageDetails {
55
55
  imageMimeType?: string;
56
56
  summary?: string;
57
57
  expandedText?: string;
58
- /** Legacy parsed title retained on older payloads; display always uses `Cursor generateImage`. */
59
- title?: string;
60
58
  collapseDetailsByDefault?: boolean;
61
59
  }
62
60
 
@@ -79,7 +77,7 @@ export interface CursorReplayActivityDetails {
79
77
  fileContentAfterWrite?: string;
80
78
  }
81
79
 
82
- /** Parsed replay details without a display title (legacy or malformed payloads). */
80
+ /** Parsed replay details without a display title. */
83
81
  export interface CursorReplayGenericFallbackDetails {
84
82
  variant: "genericFallback";
85
83
  sourceToolName: CursorReplayUnknownSourceToolName;
@@ -94,21 +92,6 @@ export type CursorReplayToolDetails =
94
92
  | CursorReplayActivityDetails
95
93
  | CursorReplayGenericFallbackDetails;
96
94
 
97
- /** @deprecated Use {@link CursorReplayNativeEditDetails}. */
98
- export type CursorReplayEditDetails = CursorReplayNativeEditDetails;
99
-
100
- /** @deprecated Use {@link CursorReplayNativeWriteDetails}. */
101
- export type CursorReplayWriteDetails = CursorReplayNativeWriteDetails;
102
-
103
- /** @deprecated Use {@link CursorReplayActivityDetails}. */
104
- export type CursorReplayTitledActivityDetails = CursorReplayActivityDetails;
105
-
106
- /** @deprecated Use {@link CursorReplayActivitySourceToolName}. */
107
- export type CursorReplayActivityCursorToolName = CursorReplayActivitySourceToolName;
108
-
109
- /** @deprecated Use {@link CursorReplayUnknownSourceToolName}. */
110
- export type CursorReplayUnknownCursorToolName = CursorReplayUnknownSourceToolName;
111
-
112
95
  export type CursorReplayActivityDetailFields = Pick<
113
96
  CursorReplayActivityDetails,
114
97
  | "summary"
@@ -147,19 +130,12 @@ function readOptionalBoolean(record: Record<string, unknown>, key: string): bool
147
130
  return typeof value === "boolean" ? value : undefined;
148
131
  }
149
132
 
150
- function readCurrentSourceToolName(record: Record<string, unknown>): string | undefined {
133
+ function readSourceToolName(record: Record<string, unknown>): string | undefined {
151
134
  const sourceToolName = readOptionalString(record, "sourceToolName");
152
135
  return sourceToolName?.trim() ? sourceToolName.trim() : undefined;
153
136
  }
154
137
 
155
- function readLegacySourceToolName(record: Record<string, unknown>): string | undefined {
156
- const sourceToolName = readCurrentSourceToolName(record);
157
- if (sourceToolName) return sourceToolName;
158
- const cursorToolName = readOptionalString(record, "cursorToolName");
159
- return cursorToolName?.trim() ? cursorToolName.trim() : undefined;
160
- }
161
-
162
- function readLegacyVariant(record: Record<string, unknown>): string | undefined {
138
+ function readVariant(record: Record<string, unknown>): string | undefined {
163
139
  const variant = readOptionalString(record, "variant");
164
140
  return variant?.trim() ? variant.trim() : undefined;
165
141
  }
@@ -191,7 +167,6 @@ function parseCursorReplayNativeWriteDetails(record: Record<string, unknown>): C
191
167
  }
192
168
 
193
169
  function parseCursorReplayGenerateImageDetails(record: Record<string, unknown>): CursorReplayGenerateImageDetails {
194
- const title = readOptionalString(record, "title");
195
170
  const collapseDetailsByDefault = readOptionalBoolean(record, "collapseDetailsByDefault");
196
171
  return {
197
172
  variant: "generateImage",
@@ -200,7 +175,6 @@ function parseCursorReplayGenerateImageDetails(record: Record<string, unknown>):
200
175
  imageMimeType: readOptionalString(record, "imageMimeType"),
201
176
  summary: readOptionalString(record, "summary"),
202
177
  expandedText: readOptionalString(record, "expandedText"),
203
- ...(title !== undefined ? { title } : {}),
204
178
  ...(collapseDetailsByDefault !== undefined ? { collapseDetailsByDefault } : {}),
205
179
  };
206
180
  }
@@ -263,37 +237,9 @@ export function resolveIncompleteReplayActivitySourceToolName(
263
237
  return resolveParseActivitySourceToolName(sourceToolName);
264
238
  }
265
239
 
266
- function hasNativeEditChanges(record: Record<string, unknown>): boolean {
267
- return Boolean(
268
- readOptionalString(record, "diffString")?.trim()
269
- || readOptionalString(record, "diff")?.trim()
270
- || readOptionalNumber(record, "linesAdded")
271
- || readOptionalNumber(record, "linesRemoved"),
272
- );
273
- }
274
-
275
- function parseLegacyEditDetails(record: Record<string, unknown>): CursorReplayToolDetails {
276
- const title = readOptionalString(record, "title")?.trim();
277
- if (title) {
278
- return parseCursorReplayActivityDetails(record, resolveParseActivitySourceToolName("edit"), title);
279
- }
280
- return parseCursorReplayNativeEditDetails(record);
281
- }
282
-
283
- function parseLegacyWriteDetails(record: Record<string, unknown>): CursorReplayToolDetails {
284
- const title = readOptionalString(record, "title")?.trim();
285
- if (title) {
286
- return parseCursorReplayActivityDetails(record, resolveParseActivitySourceToolName("write"), title);
287
- }
288
- return parseCursorReplayNativeWriteDetails(record);
289
- }
290
-
291
240
  type CursorReplayVariantParser = (record: Record<string, unknown>) => CursorReplayToolDetails | undefined;
292
241
 
293
- function parseActivityVariantDetails(
294
- record: Record<string, unknown>,
295
- readSourceToolName: (record: Record<string, unknown>) => string | undefined,
296
- ): CursorReplayActivityDetails | undefined {
242
+ function parseActivityVariantDetails(record: Record<string, unknown>): CursorReplayActivityDetails | undefined {
297
243
  const title = readOptionalString(record, "title")?.trim();
298
244
  if (!title) return undefined;
299
245
  return parseCursorReplayActivityDetails(
@@ -307,75 +253,29 @@ const CURRENT_REPLAY_VARIANT_PARSERS: Readonly<Record<CursorReplayToolDetailsVar
307
253
  nativeEdit: parseCursorReplayNativeEditDetails,
308
254
  nativeWrite: parseCursorReplayNativeWriteDetails,
309
255
  generateImage: parseCursorReplayGenerateImageDetails,
310
- activity: (record) => {
311
- if (!readCurrentSourceToolName(record) && readOptionalString(record, "cursorToolName")?.trim()) return undefined;
312
- return parseActivityVariantDetails(record, readCurrentSourceToolName);
313
- },
314
- genericFallback: (record) => parseCursorReplayGenericFallbackDetails(record, readCurrentSourceToolName(record) ?? "tool"),
256
+ activity: parseActivityVariantDetails,
257
+ genericFallback: (record) => parseCursorReplayGenericFallbackDetails(record, readSourceToolName(record) ?? "tool"),
315
258
  };
316
259
 
317
- const LEGACY_REPLAY_VARIANT_UPGRADERS: Readonly<Record<string, CursorReplayVariantParser>> = {
318
- edit: parseLegacyEditDetails,
319
- write: parseLegacyWriteDetails,
320
- titledActivity: (record) => parseActivityVariantDetails(record, readLegacySourceToolName),
321
- };
322
-
323
- export function parseStrictCurrentCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
260
+ export function parseCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
324
261
  if (!isRecord(value)) return undefined;
325
- const variant = readLegacyVariant(value);
262
+ const variant = readVariant(value);
326
263
  if (!variant) return undefined;
327
264
  return CURRENT_REPLAY_VARIANT_PARSERS[variant as CursorReplayToolDetailsVariant]?.(value);
328
265
  }
329
266
 
330
- export function upgradeLegacyCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
331
- if (!isRecord(value)) return undefined;
332
- const explicitVariant = readLegacyVariant(value);
333
- if (explicitVariant) {
334
- return LEGACY_REPLAY_VARIANT_UPGRADERS[explicitVariant]?.(value);
335
- }
336
- const sourceToolName = readLegacySourceToolName(value);
337
- if (sourceToolName === "edit") return parseLegacyEditDetails(value);
338
- if (sourceToolName === "write") return parseLegacyWriteDetails(value);
339
- if (sourceToolName === "generateImage") return parseCursorReplayGenerateImageDetails(value);
340
- const title = readOptionalString(value, "title")?.trim();
341
- if (title) {
342
- return parseCursorReplayActivityDetails(
343
- value,
344
- resolveParseActivitySourceToolName(sourceToolName ?? CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME),
345
- title,
346
- );
347
- }
348
- if (sourceToolName === undefined && hasNativeEditChanges(value)) {
349
- return parseCursorReplayNativeEditDetails(value);
350
- }
351
- return undefined;
352
- }
353
-
354
- export function parseCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
355
- return parseStrictCurrentCursorReplayToolDetails(value) ?? upgradeLegacyCursorReplayToolDetails(value);
356
- }
357
-
358
- /** @deprecated Prefer {@link parseCursorReplayToolDetails} for validated narrowing. */
359
- export const asCursorReplayToolDetails = parseCursorReplayToolDetails;
360
-
361
267
  export function buildCursorReplayNativeEditDetails(
362
268
  fields: Omit<CursorReplayNativeEditDetails, "variant">,
363
269
  ): CursorReplayNativeEditDetails {
364
270
  return { variant: "nativeEdit", ...fields };
365
271
  }
366
272
 
367
- /** @deprecated Prefer {@link buildCursorReplayNativeEditDetails}. */
368
- export const buildCursorReplayEditDetails = buildCursorReplayNativeEditDetails;
369
-
370
273
  export function buildCursorReplayNativeWriteDetails(
371
274
  fields: Omit<CursorReplayNativeWriteDetails, "variant">,
372
275
  ): CursorReplayNativeWriteDetails {
373
276
  return { variant: "nativeWrite", ...fields };
374
277
  }
375
278
 
376
- /** @deprecated Prefer {@link buildCursorReplayNativeWriteDetails}. */
377
- export const buildCursorReplayWriteDetails = buildCursorReplayNativeWriteDetails;
378
-
379
279
  export function assembleCursorReplayActivityDetails(
380
280
  sourceToolName: CursorReplayActivitySourceToolName,
381
281
  title: string,
@@ -402,10 +302,7 @@ export function assembleCursorReplayActivityDetails(
402
302
  };
403
303
  }
404
304
 
405
- /** @deprecated Prefer {@link assembleCursorReplayActivityDetails}. */
406
- export const assembleCursorReplayTitledActivityDetails = assembleCursorReplayActivityDetails;
407
-
408
- export const CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE = "Cursor generateImage" as const;
305
+ export const CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE = "Cursor image generation" as const;
409
306
 
410
307
  export function assembleCursorReplayGenerateImageDetails(
411
308
  fields: CursorReplayGenerateImageDetailFields,
@@ -430,18 +327,12 @@ export function isCursorReplayNativeEditDetails(
430
327
  return details.variant === "nativeEdit";
431
328
  }
432
329
 
433
- /** @deprecated Prefer {@link isCursorReplayNativeEditDetails}. */
434
- export const isCursorReplayEditDetails = isCursorReplayNativeEditDetails;
435
-
436
330
  export function isCursorReplayNativeWriteDetails(
437
331
  details: CursorReplayToolDetails,
438
332
  ): details is CursorReplayNativeWriteDetails {
439
333
  return details.variant === "nativeWrite";
440
334
  }
441
335
 
442
- /** @deprecated Prefer {@link isCursorReplayNativeWriteDetails}. */
443
- export const isCursorReplayWriteDetails = isCursorReplayNativeWriteDetails;
444
-
445
336
  export function isCursorReplayGenerateImageDetails(
446
337
  details: CursorReplayToolDetails,
447
338
  ): details is CursorReplayGenerateImageDetails {
@@ -454,9 +345,6 @@ export function isCursorReplayActivityDetails(
454
345
  return details.variant === "activity";
455
346
  }
456
347
 
457
- /** @deprecated Prefer {@link isCursorReplayActivityDetails}. */
458
- export const isCursorReplayTitledActivityDetails = isCursorReplayActivityDetails;
459
-
460
348
  export function isCursorReplayGenericFallbackDetails(
461
349
  details: CursorReplayToolDetails,
462
350
  ): details is CursorReplayGenericFallbackDetails {
@@ -1,18 +1,9 @@
1
1
  export {
2
- CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME,
3
2
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
4
- CURSOR_REPLAY_LEGACY_TOOL_NAMES,
5
- getCursorReplayActivityLabelKey,
6
3
  getCursorReplayActivityTitle,
7
- getCursorReplayDisplayLabel,
8
4
  getCursorReplayPromptLabel,
9
- getCursorReplaySideEffectDescription,
10
- getCursorReplayOperationLabel,
11
- getCursorReplayWrapperLabel,
12
- isCursorReplayLegacyToolName,
13
5
  isCursorReplayToolName,
14
6
  isExcludedFromCursorBridgeExposure,
15
7
  type CursorReplayActivityToolName,
16
- type CursorReplayLegacyToolName,
17
8
  type CursorReplayToolName,
18
9
  } from "./cursor-tool-presentation-registry.js";
@@ -57,8 +57,6 @@ export const CURSOR_REPLAY_ACTIVITY_TOOL_NAME = "cursor" as const;
57
57
 
58
58
  export type CursorWebToolKind = "webSearch" | "webFetch";
59
59
 
60
- export type CursorReplaySideEffectCategory = "file_mutations" | "real_tool_work";
61
-
62
60
  export type CursorToolLifecycleLabelKind =
63
61
  | "task"
64
62
  | "shell"
@@ -96,10 +94,6 @@ export interface CursorToolPresentationSpec {
96
94
  normalizedName: CursorReplaySourceToolName;
97
95
  /** Raw SDK/host names that resolve to this tool via {@link normalizeCursorToolName}. */
98
96
  nameAliases?: readonly string[];
99
- replayLegacyName?: string;
100
- /** Human-readable SDK operation label for replay-only tool descriptions when it differs from {@link normalizedName}. */
101
- replayOperationLabel?: string;
102
- promptLabel: string;
103
97
  displayLabel: string;
104
98
  visibility: CursorToolVisibilityPolicy;
105
99
  webKind?: CursorWebToolKind;
@@ -107,52 +101,43 @@ export interface CursorToolPresentationSpec {
107
101
  webNamePatterns?: readonly RegExp[];
108
102
  lifecycleLabelKind?: CursorToolLifecycleLabelKind;
109
103
  replayCallSummary?: CursorReplayCallSummaryBuilder;
110
- /** Short label for replay-only tool definitions (for example `edit` for `cursor_edit`). */
111
- replayWrapperLabel?: string;
112
- /** Whether replay-only wrappers describe file mutations or other recorded tool work. */
113
- replaySideEffectCategory?: CursorReplaySideEffectCategory;
114
104
  activityReplay?: CursorToolActivityReplaySpec;
115
105
  generateImageReplay?: CursorToolGenerateImageReplaySpec;
116
106
  }
117
107
 
118
108
  const WEB_SEARCH_NAME_PATTERN =
119
- /^(?:web[-_ ]?search|search[-_ ]?web|websearch|browser[-_ ]?search|cursor[-_ ]?web[-_ ]?search)$/i;
109
+ /^(?:web[-_ ]?search|search[-_ ]?web|websearch|browser[-_ ]?search)$/i;
120
110
  const WEB_FETCH_NAME_PATTERN =
121
- /^(?:web[-_ ]?fetch|fetch[-_ ]?web|webfetch|browser[-_ ]?fetch|fetch[-_ ]?url|cursor[-_ ]?web[-_ ]?fetch)$/i;
111
+ /^(?:web[-_ ]?fetch|fetch[-_ ]?web|webfetch|browser[-_ ]?fetch|fetch[-_ ]?url)$/i;
122
112
 
123
113
  export const CURSOR_TOOL_PRESENTATION_SPECS = [
124
114
  {
125
115
  normalizedName: "read",
126
116
  nameAliases: ["read_file"],
127
- promptLabel: "read",
128
117
  displayLabel: "read",
129
118
  visibility: { incompleteTitle: "Cursor read", fastLocalDiscovery: true },
130
119
  },
131
120
  {
132
121
  normalizedName: "grep",
133
122
  nameAliases: ["grep_search", "search"],
134
- promptLabel: "grep",
135
123
  displayLabel: "grep",
136
124
  visibility: { incompleteTitle: "Cursor grep", fastLocalDiscovery: true },
137
125
  },
138
126
  {
139
127
  normalizedName: "glob",
140
128
  nameAliases: ["file_search"],
141
- promptLabel: "glob",
142
129
  displayLabel: "glob",
143
130
  visibility: { incompleteTitle: "Cursor find", fastLocalDiscovery: true },
144
131
  },
145
132
  {
146
133
  normalizedName: "ls",
147
134
  nameAliases: ["list_dir"],
148
- promptLabel: "ls",
149
135
  displayLabel: "ls",
150
136
  visibility: { incompleteTitle: "Cursor ls", fastLocalDiscovery: true },
151
137
  },
152
138
  {
153
139
  normalizedName: "shell",
154
140
  nameAliases: ["run_terminal_cmd", "terminal", "bash"],
155
- promptLabel: "shell",
156
141
  displayLabel: "shell",
157
142
  visibility: {
158
143
  incompleteTitle: "Cursor shell",
@@ -174,29 +159,19 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
174
159
  "notebook_edit",
175
160
  "notebookedit",
176
161
  ],
177
- replayLegacyName: "cursor_edit",
178
- promptLabel: "Cursor edit",
179
162
  displayLabel: "Cursor edit",
180
163
  visibility: {},
181
- replayWrapperLabel: "edit",
182
- replaySideEffectCategory: "file_mutations",
183
164
  replayCallSummary: withActivitySummaryFallback(summarizeReplayPath),
184
165
  },
185
166
  {
186
167
  normalizedName: "write",
187
168
  nameAliases: ["write_file", "writefile"],
188
- replayLegacyName: "cursor_write",
189
- promptLabel: "Cursor write",
190
169
  displayLabel: "Cursor write",
191
170
  visibility: {},
192
- replayWrapperLabel: "write",
193
- replaySideEffectCategory: "file_mutations",
194
171
  replayCallSummary: withActivitySummaryFallback(summarizeReplayPath),
195
172
  },
196
173
  {
197
174
  normalizedName: "delete",
198
- replayLegacyName: "cursor_delete",
199
- promptLabel: "Cursor delete",
200
175
  displayLabel: "Cursor delete",
201
176
  visibility: {},
202
177
  replayCallSummary: withActivitySummaryFallback(summarizeReplayPath),
@@ -207,8 +182,6 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
207
182
  },
208
183
  {
209
184
  normalizedName: "readLints",
210
- replayLegacyName: "cursor_read_lints",
211
- promptLabel: "Cursor diagnostics",
212
185
  displayLabel: "Cursor diagnostics",
213
186
  visibility: {},
214
187
  replayCallSummary: withActivitySummaryFallback(summarizeReplayReadLints),
@@ -219,8 +192,6 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
219
192
  },
220
193
  {
221
194
  normalizedName: "updateTodos",
222
- replayLegacyName: "cursor_update_todos",
223
- promptLabel: "Cursor todos",
224
195
  displayLabel: "Cursor todos",
225
196
  visibility: { lifecycleEligible: true },
226
197
  lifecycleLabelKind: "updateTodos",
@@ -232,8 +203,6 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
232
203
  },
233
204
  {
234
205
  normalizedName: "createPlan",
235
- replayLegacyName: "cursor_create_plan",
236
- promptLabel: "Cursor plan",
237
206
  displayLabel: "Cursor plan",
238
207
  visibility: { lifecycleEligible: true },
239
208
  lifecycleLabelKind: "createPlan",
@@ -245,8 +214,6 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
245
214
  },
246
215
  {
247
216
  normalizedName: "task",
248
- replayLegacyName: "cursor_task",
249
- promptLabel: "Cursor task",
250
217
  displayLabel: "Cursor task",
251
218
  visibility: { lifecycleEligible: true },
252
219
  lifecycleLabelKind: "task",
@@ -258,8 +225,6 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
258
225
  },
259
226
  {
260
227
  normalizedName: "generateImage",
261
- replayLegacyName: "cursor_generate_image",
262
- promptLabel: "Cursor image generation",
263
228
  displayLabel: "Cursor image generation",
264
229
  visibility: { lifecycleEligible: true },
265
230
  lifecycleLabelKind: "generateImage",
@@ -273,9 +238,6 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
273
238
  },
274
239
  {
275
240
  normalizedName: "mcp",
276
- replayLegacyName: "cursor_mcp",
277
- replayOperationLabel: "MCP",
278
- promptLabel: "Cursor MCP",
279
241
  displayLabel: "Cursor MCP",
280
242
  visibility: { lifecycleEligible: true },
281
243
  lifecycleLabelKind: "mcp",
@@ -287,8 +249,6 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
287
249
  },
288
250
  {
289
251
  normalizedName: "semSearch",
290
- replayLegacyName: "cursor_sem_search",
291
- promptLabel: "Cursor semantic search",
292
252
  displayLabel: "Cursor semantic search",
293
253
  visibility: { lifecycleEligible: true },
294
254
  lifecycleLabelKind: "semSearch",
@@ -300,8 +260,6 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
300
260
  },
301
261
  {
302
262
  normalizedName: "recordScreen",
303
- replayLegacyName: "cursor_record_screen",
304
- promptLabel: "Cursor screen recording",
305
263
  displayLabel: "Cursor screen recording",
306
264
  visibility: { lifecycleEligible: true },
307
265
  lifecycleLabelKind: "recordScreen",
@@ -314,9 +272,6 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
314
272
  {
315
273
  normalizedName: "webSearch",
316
274
  nameAliases: ["websearch", "web_search", "web-search"],
317
- replayLegacyName: "cursor_web_search",
318
- replayOperationLabel: "web search",
319
- promptLabel: "Cursor web search",
320
275
  displayLabel: "Cursor web search",
321
276
  visibility: { lifecycleEligible: true },
322
277
  webKind: "webSearch",
@@ -333,9 +288,6 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
333
288
  {
334
289
  normalizedName: "webFetch",
335
290
  nameAliases: ["webfetch", "web_fetch", "web-fetch"],
336
- replayLegacyName: "cursor_web_fetch",
337
- replayOperationLabel: "web fetch",
338
- promptLabel: "Cursor web fetch",
339
291
  displayLabel: "Cursor web fetch",
340
292
  visibility: { lifecycleEligible: true },
341
293
  webKind: "webFetch",
@@ -354,56 +306,19 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
354
306
  type CursorToolPresentationSpecEntry = (typeof CURSOR_TOOL_PRESENTATION_SPECS)[number];
355
307
 
356
308
  export type CursorNormalizedToolName = CursorReplaySourceToolName;
309
+ export type CursorReplayToolName = typeof CURSOR_REPLAY_ACTIVITY_TOOL_NAME;
357
310
 
358
- export type CursorReplayLegacyToolName = Extract<
359
- CursorToolPresentationSpecEntry,
360
- { readonly replayLegacyName: string }
361
- >["replayLegacyName"];
362
-
363
- export type CursorReplayToolName = typeof CURSOR_REPLAY_ACTIVITY_TOOL_NAME | CursorReplayLegacyToolName;
364
-
365
- type CursorToolPresentationSpecWithReplayLegacy = Extract<
311
+ type CursorToolPresentationSpecWithNeutralActivity = Extract<
366
312
  CursorToolPresentationSpecEntry,
367
- { readonly replayLegacyName: string }
368
- >;
313
+ { readonly activityReplay: CursorToolActivityReplaySpec } | { readonly generateImageReplay: CursorToolGenerateImageReplaySpec }
314
+ > | Extract<CursorToolPresentationSpecEntry, { readonly normalizedName: "edit" | "write" }>;
369
315
 
370
- const CURSOR_REPLAY_ACTIVITY_SIDE_EFFECT_CATEGORY: CursorReplaySideEffectCategory = "file_mutations";
316
+ export type CursorReplayActivityToolName = CursorToolPresentationSpecWithNeutralActivity["normalizedName"];
371
317
 
372
- function hasReplayLegacyName(spec: CursorToolPresentationSpecEntry): spec is CursorToolPresentationSpecWithReplayLegacy {
373
- return "replayLegacyName" in spec;
318
+ function hasNeutralActivityTitle(spec: CursorToolPresentationSpec): boolean {
319
+ return Boolean(spec.activityReplay || spec.generateImageReplay || spec.normalizedName === "edit" || spec.normalizedName === "write");
374
320
  }
375
321
 
376
- type ReplayActivityLabelKeysFromSpecs<Specs extends readonly CursorToolPresentationSpecEntry[]> = {
377
- [K in Extract<Specs[number], { readonly replayLegacyName: string }>["normalizedName"]]: Extract<
378
- Specs[number],
379
- { readonly replayLegacyName: string; normalizedName: K }
380
- >["replayLegacyName"];
381
- };
382
-
383
- function buildReplayActivityLabelKeysByToolName<const Specs extends readonly CursorToolPresentationSpecEntry[]>(
384
- specs: Specs,
385
- ): ReplayActivityLabelKeysFromSpecs<Specs> {
386
- const labelKeys: Record<string, CursorReplayLegacyToolName> = {};
387
- for (const spec of specs) {
388
- if (!hasReplayLegacyName(spec)) continue;
389
- labelKeys[spec.normalizedName] = spec.replayLegacyName;
390
- }
391
- return labelKeys as ReplayActivityLabelKeysFromSpecs<Specs>;
392
- }
393
-
394
- const CURSOR_REPLAY_SPECS = CURSOR_TOOL_PRESENTATION_SPECS.filter(hasReplayLegacyName);
395
-
396
- /** Stable registration order for native replay tool wrappers (registry declaration order). */
397
- export const CURSOR_REPLAY_LEGACY_TOOL_NAMES: readonly CursorReplayLegacyToolName[] = CURSOR_REPLAY_SPECS.map(
398
- (spec) => spec.replayLegacyName,
399
- );
400
-
401
- export const CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME = buildReplayActivityLabelKeysByToolName(
402
- CURSOR_TOOL_PRESENTATION_SPECS,
403
- );
404
-
405
- export type CursorReplayActivityToolName = keyof typeof CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME;
406
-
407
322
  const SPECS_BY_NORMALIZED_NAME = new Map<string, CursorToolPresentationSpec>(
408
323
  CURSOR_TOOL_PRESENTATION_SPECS.map((spec) => [spec.normalizedName, spec]),
409
324
  );
@@ -412,10 +327,6 @@ const SPECS_BY_NORMALIZED_KEY = new Map<string, CursorToolPresentationSpec>(
412
327
  CURSOR_TOOL_PRESENTATION_SPECS.map((spec) => [spec.normalizedName.toLowerCase(), spec]),
413
328
  );
414
329
 
415
- const SPECS_BY_REPLAY_LEGACY_NAME = new Map<CursorReplayLegacyToolName, CursorToolPresentationSpec>(
416
- CURSOR_REPLAY_SPECS.map((spec) => [spec.replayLegacyName, spec]),
417
- );
418
-
419
330
  const ALIAS_TO_NORMALIZED_NAME = new Map<string, CursorNormalizedToolName>(
420
331
  CURSOR_TOOL_PRESENTATION_SPECS.flatMap((spec) => {
421
332
  const aliases = "nameAliases" in spec ? spec.nameAliases : undefined;
@@ -437,11 +348,7 @@ export function getCursorToolPresentationSpec(
437
348
  ): CursorToolPresentationSpec | undefined {
438
349
  const trimmed = name.trim();
439
350
  if (!trimmed) return undefined;
440
- return (
441
- SPECS_BY_NORMALIZED_NAME.get(trimmed) ??
442
- SPECS_BY_NORMALIZED_KEY.get(trimmed.toLowerCase()) ??
443
- (isCursorReplayLegacyToolName(trimmed) ? SPECS_BY_REPLAY_LEGACY_NAME.get(trimmed) : undefined)
444
- );
351
+ return SPECS_BY_NORMALIZED_NAME.get(trimmed) ?? SPECS_BY_NORMALIZED_KEY.get(trimmed.toLowerCase());
445
352
  }
446
353
 
447
354
  export function normalizeCursorToolName(name: string): string {
@@ -464,64 +371,21 @@ export function classifyCursorWebToolKind(name: string | undefined): CursorWebTo
464
371
  return spec?.webKind;
465
372
  }
466
373
 
467
- const CURSOR_REPLAY_LEGACY_TOOL_NAME_SET: ReadonlySet<string> = new Set(CURSOR_REPLAY_LEGACY_TOOL_NAMES);
468
-
469
- export function isCursorReplayLegacyToolName(toolName: string): toolName is CursorReplayLegacyToolName {
470
- return CURSOR_REPLAY_LEGACY_TOOL_NAME_SET.has(toolName);
471
- }
472
-
473
374
  export function isCursorReplayToolName(toolName: string): toolName is CursorReplayToolName {
474
- return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME || isCursorReplayLegacyToolName(toolName);
375
+ return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME;
475
376
  }
476
377
 
477
378
  export function isExcludedFromCursorBridgeExposure(toolName: string): boolean {
478
- return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME || isCursorReplayLegacyToolName(toolName);
479
- }
480
-
481
- export function getCursorReplayWrapperLabel(toolName: CursorReplayToolName): string {
482
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return getCursorReplayDisplayLabel(toolName);
483
- const spec = SPECS_BY_REPLAY_LEGACY_NAME.get(toolName);
484
- return spec?.replayWrapperLabel ?? getCursorReplayDisplayLabel(toolName);
485
- }
486
-
487
- export function getCursorReplaySideEffectCategory(toolName: CursorReplayToolName): CursorReplaySideEffectCategory {
488
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return CURSOR_REPLAY_ACTIVITY_SIDE_EFFECT_CATEGORY;
489
- return SPECS_BY_REPLAY_LEGACY_NAME.get(toolName)?.replaySideEffectCategory ?? "real_tool_work";
490
- }
491
-
492
- export function getCursorReplaySideEffectDescription(toolName: CursorReplayToolName): string {
493
- return getCursorReplaySideEffectCategory(toolName) === "file_mutations" ? "file mutations" : "real tool work";
494
- }
495
-
496
- export function getCursorReplayOperationLabel(toolName: CursorReplayLegacyToolName): string {
497
- const spec = SPECS_BY_REPLAY_LEGACY_NAME.get(toolName);
498
- return spec?.replayOperationLabel ?? spec?.normalizedName ?? toolName;
379
+ return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME;
499
380
  }
500
381
 
501
382
  export function getCursorReplayPromptLabel(toolName: string): string {
502
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
503
- if (isCursorReplayLegacyToolName(toolName)) {
504
- const spec = SPECS_BY_REPLAY_LEGACY_NAME.get(toolName);
505
- return spec?.promptLabel ?? toolName;
506
- }
507
- return toolName;
508
- }
509
-
510
- export function getCursorReplayDisplayLabel(toolName: CursorReplayToolName): string {
511
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
512
- const spec = SPECS_BY_REPLAY_LEGACY_NAME.get(toolName);
513
- return spec?.displayLabel ?? toolName;
514
- }
515
-
516
- export function getCursorReplayActivityLabelKey(toolName: string): CursorReplayLegacyToolName | undefined {
517
- const spec = getCursorToolPresentationSpec(toolName);
518
- if (!spec?.replayLegacyName || !isCursorReplayLegacyToolName(spec.replayLegacyName)) return undefined;
519
- return spec.replayLegacyName;
383
+ return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "Cursor activity" : toolName;
520
384
  }
521
385
 
522
386
  export function getCursorReplayActivityTitle(toolName: string): string | undefined {
523
387
  const spec = getCursorToolPresentationSpec(toolName);
524
- if (!spec?.replayLegacyName) return undefined;
388
+ if (!spec || !hasNeutralActivityTitle(spec)) return undefined;
525
389
  return spec.displayLabel;
526
390
  }
527
391
 
@@ -546,11 +410,11 @@ export function getCursorToolGenerateImageReplaySpec(normalizedKey: string): Cur
546
410
  }
547
411
 
548
412
  export function getCursorReplayCallSummary(
549
- toolName: CursorReplayToolName,
413
+ toolName: CursorReplayToolName | CursorReplaySourceToolName,
550
414
  args: CursorReplaySummaryArgs | undefined,
551
415
  ): string | undefined {
552
416
  if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) {
553
417
  return readCursorReplaySummaryString(args, "activitySummary") ?? summarizeReplayGenericActivity(args);
554
418
  }
555
- return SPECS_BY_REPLAY_LEGACY_NAME.get(toolName)?.replayCallSummary?.(args);
419
+ return getCursorToolPresentationSpec(toolName)?.replayCallSummary?.(args);
556
420
  }
@@ -1,15 +1,12 @@
1
1
  import {
2
2
  CURSOR_KNOWN_NORMALIZED_TOOL_NAMES,
3
3
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
4
- getCursorReplayActivityLabelKey,
5
4
  getCursorReplayActivityTitle,
6
5
  getCursorReplayCallSummary,
7
- getCursorReplayDisplayLabel,
8
6
  getCursorToolActivityReplaySpec,
9
7
  getCursorToolGenerateImageReplaySpec,
10
8
  type CursorNormalizedToolName,
11
9
  type CursorReplayActivityToolName,
12
- type CursorReplayLegacyToolName,
13
10
  } from "./cursor-tool-presentation-registry.js";
14
11
  import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
15
12
  import {
@@ -88,12 +85,6 @@ interface ToolDisplaySpec {
88
85
  buildPiToolDisplay: (context: ToolDisplayContext) => CursorPiToolDisplay;
89
86
  }
90
87
 
91
- function requireReplayActivityLabelKey(normalizedName: CursorReplayActivityToolName): CursorReplayLegacyToolName {
92
- const labelKey = getCursorReplayActivityLabelKey(normalizedName);
93
- if (!labelKey) throw new Error(`Missing replay activity label for ${normalizedName}`);
94
- return labelKey;
95
- }
96
-
97
88
  function textToolResult(text: string, details?: unknown): PiToolDisplayResult {
98
89
  return { content: [{ type: "text", text }], details };
99
90
  }
@@ -112,10 +103,10 @@ function buildCursorActivityDisplayArgs(
112
103
  }
113
104
 
114
105
  function buildRegistryReplaySummary(
115
- labelKey: CursorReplayLegacyToolName,
106
+ sourceToolName: CursorReplayActivityToolName,
116
107
  args: CursorReplaySummaryArgs,
117
108
  ): string | undefined {
118
- return getCursorReplayCallSummary(labelKey, args);
109
+ return getCursorReplayCallSummary(sourceToolName, args);
119
110
  }
120
111
 
121
112
  function buildReplaySummaryDisplay(
@@ -151,10 +142,9 @@ function buildActivityReplayDisplay(
151
142
  const spec = TOOL_DISPLAY_SPECS[sourceToolName];
152
143
  const activity = getCursorToolActivityReplaySpec(sourceToolName);
153
144
  if (!activity) throw new Error(`Missing activity replay spec for ${sourceToolName}`);
154
- const labelKey = requireReplayActivityLabelKey(sourceToolName);
155
- const activityTitle = getCursorReplayDisplayLabel(labelKey);
145
+ const activityTitle = getCursorReplayActivityTitle(sourceToolName) ?? buildGenericUnknownToolActivityTitle(sourceToolName);
156
146
  const replayArgs = activity.buildActivityArgs(context);
157
- const activitySummary = buildRegistryReplaySummary(labelKey, replayArgs);
147
+ const activitySummary = buildRegistryReplaySummary(sourceToolName, replayArgs);
158
148
  const activityArgs = buildCursorActivityDisplayArgs({ ...replayArgs }, activityTitle, activitySummary);
159
149
  const contentText = spec.formatTranscript(context).trimEnd();
160
150
  const activityFields = activity.buildDetails(context, contentText);
@@ -173,10 +163,9 @@ function buildGenerateImageReplayDisplay(context: ToolDisplayContext): CursorPiT
173
163
  const spec = TOOL_DISPLAY_SPECS.generateImage;
174
164
  const replay = getCursorToolGenerateImageReplaySpec("generateImage");
175
165
  if (!replay) throw new Error("Missing generate image replay spec");
176
- const labelKey = requireReplayActivityLabelKey("generateImage");
177
- const activityTitle = getCursorReplayDisplayLabel(labelKey);
166
+ const activityTitle = getCursorReplayActivityTitle("generateImage") ?? "Cursor image generation";
178
167
  const replayArgs = replay.buildActivityArgs(context);
179
- const activitySummary = buildRegistryReplaySummary(labelKey, replayArgs);
168
+ const activitySummary = buildRegistryReplaySummary("generateImage", replayArgs);
180
169
  const activityArgs = buildCursorActivityDisplayArgs({ ...replayArgs }, activityTitle, activitySummary);
181
170
  const contentText = spec.formatTranscript(context).trimEnd();
182
171
  const details = assembleCursorReplayGenerateImageDetails(
@@ -201,11 +190,7 @@ function buildGenericPiToolDisplay(context: ToolDisplayContext): CursorPiToolDis
201
190
  const fallbackBody = contentText.includes("\n\n") ? contentText.slice(contentText.indexOf("\n\n") + 2) : "";
202
191
  const activitySummary =
203
192
  result.status === "error" ? undefined : firstNonEmptyLine(fallbackBody);
204
- const activityArgs = buildCursorActivityDisplayArgs(
205
- { cursorToolName: displayName === "unknown" ? "tool" : displayName },
206
- activityTitle,
207
- activitySummary,
208
- );
193
+ const activityArgs = buildCursorActivityDisplayArgs({}, activityTitle, activitySummary);
209
194
  const summary =
210
195
  result.status === "error"
211
196
  ? undefined