pi-cursor-sdk 0.1.37 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/docs/cursor-native-tool-replay.md +3 -3
  3. package/package.json +1 -1
  4. package/scripts/platform-smoke/card-detect.mjs +1 -1
  5. package/src/context-window-cache.ts +10 -14
  6. package/src/context.ts +1 -1
  7. package/src/cursor-agent-message-web-tools.ts +2 -1
  8. package/src/cursor-agents-context-registration.ts +18 -0
  9. package/src/cursor-agents-context.ts +21 -30
  10. package/src/cursor-edit-diff.ts +4 -2
  11. package/src/cursor-fallback-warning.ts +22 -0
  12. package/src/cursor-incomplete-tool-visibility.ts +5 -10
  13. package/src/cursor-live-run-coordinator.ts +1 -1
  14. package/src/cursor-mcp-timeout-override.ts +0 -2
  15. package/src/cursor-model-lifecycle.ts +72 -0
  16. package/src/cursor-native-replay-routing.ts +1 -1
  17. package/src/cursor-native-replay-trace.ts +1 -1
  18. package/src/cursor-native-tool-display-registration.ts +16 -28
  19. package/src/cursor-native-tool-display-replay.ts +4 -21
  20. package/src/cursor-native-tool-display-state.ts +1 -1
  21. package/src/cursor-native-tool-display-tools.ts +10 -17
  22. package/src/cursor-native-tool-names.ts +16 -0
  23. package/src/cursor-pi-tool-bridge-env.ts +12 -0
  24. package/src/cursor-pi-tool-bridge-mcp.ts +16 -21
  25. package/src/cursor-pi-tool-bridge-run.ts +5 -5
  26. package/src/cursor-pi-tool-bridge-server.ts +8 -3
  27. package/src/cursor-pi-tool-bridge-snapshot.ts +7 -13
  28. package/src/cursor-pi-tool-bridge.ts +7 -7
  29. package/src/cursor-provider-lazy.ts +51 -0
  30. package/src/cursor-provider-live-run-drain.ts +1 -1
  31. package/src/cursor-provider-run-finalizer.ts +5 -5
  32. package/src/cursor-provider-run-outcome.ts +0 -1
  33. package/src/cursor-provider-turn-coordinator.ts +4 -5
  34. package/src/cursor-provider-turn-display-router.ts +5 -1
  35. package/src/cursor-provider-turn-emit.ts +1 -1
  36. package/src/cursor-provider-turn-lifecycle-emitter.ts +1 -5
  37. package/src/cursor-provider-turn-prepare.ts +13 -9
  38. package/src/cursor-provider-turn-runner.ts +3 -11
  39. package/src/cursor-provider-turn-sdk-normalizer.ts +28 -5
  40. package/src/cursor-provider-turn-send.ts +7 -2
  41. package/src/cursor-provider-turn-types.ts +1 -3
  42. package/src/cursor-provider.ts +3 -2
  43. package/src/cursor-question-tool.ts +5 -18
  44. package/src/cursor-record-utils.ts +42 -0
  45. package/src/cursor-replay-activity-builders.ts +16 -122
  46. package/src/cursor-replay-tool-details.ts +52 -80
  47. package/src/cursor-sdk-event-debug.ts +6 -6
  48. package/src/cursor-sensitive-text.ts +4 -4
  49. package/src/cursor-session-agent-lifecycle.ts +47 -0
  50. package/src/cursor-session-agent.ts +9 -47
  51. package/src/cursor-session-scope.ts +23 -4
  52. package/src/cursor-setting-sources.ts +8 -8
  53. package/src/cursor-skill-tool.ts +25 -32
  54. package/src/cursor-state.ts +66 -45
  55. package/src/cursor-tool-lifecycle.ts +16 -9
  56. package/src/cursor-tool-presentation-registry.ts +27 -18
  57. package/src/cursor-tool-result-display-readers.ts +185 -0
  58. package/src/cursor-tool-transcript.ts +17 -33
  59. package/src/cursor-tool-visibility.ts +9 -1
  60. package/src/cursor-transcript-tool-formatters.ts +23 -172
  61. package/src/cursor-transcript-tool-specs.ts +16 -41
  62. package/src/cursor-transcript-utils.ts +2 -34
  63. package/src/cursor-usage-accounting.ts +0 -6
  64. package/src/cursor-web-tool-activity.ts +4 -12
  65. package/src/cursor-web-tool-args.ts +1 -9
  66. package/src/index.ts +15 -16
  67. package/src/model-discovery.ts +5 -4
  68. package/src/model-list-cache.ts +37 -38
  69. package/src/cursor-native-tool-display.ts +0 -10
  70. package/src/cursor-provider-turn-api-key.ts +0 -1
  71. package/src/cursor-provider-turn-message-offset.ts +0 -15
  72. package/src/cursor-session-cwd.ts +0 -28
  73. package/src/cursor-tool-names.ts +0 -9
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.38 - 2026-06-08
6
+
7
+ ### Added
8
+
9
+ - Add shared Cursor replay result readers so transcript formatting, native replay cards, and activity builders consume the same MCP-like content/diff/file-preview extraction logic.
10
+ - Add a canonical Cursor model lifecycle sync helper for session start, before-agent-start, model selection, and turn start registration paths.
11
+ - Add lazy Cursor provider registration so extension startup can register models and commands without importing the Cursor SDK runtime until the provider is invoked.
12
+ - Add shared Cursor native tool-name and pi-tool-bridge environment helpers for provider/runtime registration code.
13
+
14
+ ### Changed
15
+
16
+ - Centralize Cursor tool presentation ownership in the typed presentation registry, including labels, aliases, lifecycle titles, replay metadata, side-effect policies, and web-tool classification.
17
+ - Consolidate Cursor session cwd, session file/id, generation, and scope-key handling in `cursor-session-scope`; remove the older cwd/message-offset helper split.
18
+ - Simplify Cursor session-agent lifecycle invalidation on model select, compaction preparation, tree navigation, shutdown, and scope changes.
19
+ - Refine Cursor tool lifecycle/replay display routing so completed replay cards, inactive traces, native replay activation, and duplicate step/delta completions share one display path.
20
+ - Keep Cursor agents-context dedup and fallback-catalog warning registration model-scoped through the shared lifecycle helper.
21
+ - Keep edit/write replay previews on the shared structured diff/file preview renderers while retaining SDK expanded-text fallback behavior.
22
+ - Update maintainer docs and repo map entries for the new ownership boundaries.
23
+
24
+ ### Fixed
25
+
26
+ - Clear started Cursor tool calls when a completed delta reports the same tool under a different SDK call id, preventing stale native replay edit starts from surfacing as `Cursor edit did not complete` after successful final text.
27
+ - Keep Cursor agents-context dedup registration in a tracked module so clean package builds resolve `src/index.ts` imports.
28
+ - Accept Windows-rendered absolute `README.md` paths in platform-smoke grep-card detection without weakening prompt false-positive checks.
29
+ - Preserve Cursor skill activation and question-tool registration through lazy provider/runtime import boundaries.
30
+ - Preserve fast local discovery incomplete-tool suppression while still surfacing aborts, SDK failures, and no-text incomplete runs.
31
+
5
32
  ## 0.1.37 - 2026-06-06
6
33
 
7
34
  ### Changed
@@ -70,7 +70,7 @@ Edit and write activity replays through pi-facing `edit` and `write` cards only
70
70
 
71
71
  Source of truth for SDK tool names: `@cursor/sdk@1.0.17` conversation `ToolType` values and https://cursor.com/docs/sdk/typescript
72
72
 
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).
73
+ Implementation owners: `src/cursor-tool-presentation-registry.ts` (canonical names, labels, visibility, replay policy, bridge exclusions for internal replay wrappers, alias normalization, and display-spec key completeness), `src/cursor-transcript-tool-specs.ts` (registry-keyed display implementations for transcript formatting and pi display builders), `src/cursor-native-tool-display-replay.ts` (replay card rendering derived from registry replay metadata), and `src/cursor-web-tool-activity.ts` (MCP/web alias remapping before display lookup).
74
74
 
75
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
 
@@ -98,7 +98,7 @@ This matrix covers **Cursor native tool replay only**. It does not describe the
98
98
  | *(host/MCP alias)* `WebFetch` / `web_fetch` / similar | neutral activity | `cursor` | Collapsed label **Cursor web fetch**; display-only Cursor web access reported by the SDK, not an executable pi web tool |
99
99
  | _(no spec; future/unknown SDK name)_ | neutral activity | `cursor` | Collapsed label **Cursor** plus SDK tool name via `buildGenericPiToolDisplay()`; bounded fallback transcript only |
100
100
 
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.
101
+ **Unknown/future fallback path:** SDK tool names with no registry-backed display implementation 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()` on the display implementation table 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
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
 
@@ -106,7 +106,7 @@ Neutral activity rows use pi tool name `cursor` with `activityTitle` / `activity
106
106
 
107
107
  ## Runtime alias normalization
108
108
 
109
- Before lookup in `TOOL_DISPLAY_SPECS`, completed SDK tool names pass through `normalizeToolName()` in `src/cursor-transcript-utils.ts`. Documented aliases:
109
+ Before display lookup, completed SDK tool names pass through `normalizeCursorToolName()` in `src/cursor-tool-presentation-registry.ts`; MCP web tool names are additionally remapped by `resolveTranscriptToolName()` in `src/cursor-web-tool-activity.ts`. Documented aliases:
110
110
 
111
111
  | Runtime alias | Canonical SDK name |
112
112
  | --- | --- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
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",
@@ -11,7 +11,7 @@ import { resolve } from "node:path";
11
11
 
12
12
  const CARD_PATTERNS = [
13
13
  { id: "read", pattern: /^\s*read (?:\.\/)?package\.json\s*$/i },
14
- { id: "grep", pattern: /^\s*grep \/pi-cursor-sdk\/ in README\.md\s*$/i },
14
+ { id: "grep", pattern: /^\s*grep \/pi-cursor-sdk\/ in\s+(?:(?:\S+[\\/])?README\.md)\s*$/i },
15
15
  { id: "find", pattern: /^\s*find README\.md in\s+\S+/i },
16
16
  { id: "list", pattern: /^\s*(?:find \* in src|find src\/\* in \.|Get-ChildItem -Name \.\/src)\s*/i },
17
17
  { id: "shell-success", pattern: /^\s*cursor visual smoke\s*$/i },
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
  import { BUNDLED_CONTEXT_WINDOWS } from "./bundled-context-windows.js";
5
+ import { asRecord } from "./cursor-record-utils.js";
5
6
 
6
7
  const CONTEXT_WINDOW_CACHE_FILE = "cursor-sdk-context-windows.json";
7
8
  let userContextWindowOverrideLoadCount = 0;
@@ -18,18 +19,16 @@ function isPositiveInteger(value: unknown): value is number {
18
19
  return typeof value === "number" && Number.isInteger(value) && value > 0;
19
20
  }
20
21
 
21
- function isRecord(value: unknown): value is Record<string, unknown> {
22
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
23
- }
24
-
25
22
  function parseContextWindowCacheFile(value: unknown): ContextWindowCacheFile | undefined {
26
- if (!isRecord(value)) return undefined;
27
- const { contextWindows } = value;
23
+ const record = asRecord(value);
24
+ if (!record) return undefined;
25
+ const { contextWindows } = record;
28
26
  if (contextWindows === undefined) return {};
29
- if (!isRecord(contextWindows)) return undefined;
27
+ const contextWindowRecord = asRecord(contextWindows);
28
+ if (!contextWindowRecord) return undefined;
30
29
  return {
31
30
  contextWindows: Object.fromEntries(
32
- Object.entries(contextWindows).filter((entry): entry is [string, number] => isPositiveInteger(entry[1])),
31
+ Object.entries(contextWindowRecord).filter((entry): entry is [string, number] => isPositiveInteger(entry[1])),
33
32
  ),
34
33
  };
35
34
  }
@@ -68,12 +67,9 @@ export function getCachedContextWindow(modelId: string): number | undefined {
68
67
  }
69
68
 
70
69
  export function getCheckpointContextWindow(checkpoint: unknown): number | undefined {
71
- if (!isRecord(checkpoint)) return undefined;
72
- const { tokenDetails } = checkpoint;
73
- if (!isRecord(tokenDetails)) return undefined;
74
- const { maxTokens } = tokenDetails;
75
- if (!isPositiveInteger(maxTokens)) return undefined;
76
- return maxTokens;
70
+ const tokenDetails = asRecord(checkpoint)?.tokenDetails;
71
+ const maxTokens = asRecord(tokenDetails)?.maxTokens;
72
+ return isPositiveInteger(maxTokens) ? maxTokens : undefined;
77
73
  }
78
74
 
79
75
  export function saveCachedContextWindow(modelId: string, contextWindow: number): void {
package/src/context.ts CHANGED
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import type { Context, Message, ToolCall } from "@earendil-works/pi-ai";
3
3
  import { convertToLlm } from "@earendil-works/pi-coding-agent";
4
4
  import type { SDKImage } from "@cursor/sdk";
5
- import { getCursorReplayPromptLabel } from "./cursor-tool-names.js";
5
+ import { getCursorReplayPromptLabel } from "./cursor-tool-presentation-registry.js";
6
6
 
7
7
  export interface CursorPrompt {
8
8
  text: string;
@@ -1,5 +1,6 @@
1
1
  import type { AgentMessage } from "@cursor/sdk";
2
- import { asRecord, getArray, getString, stringifyUnknown } from "./cursor-transcript-utils.js";
2
+ import { asRecord, getArray, getString } from "./cursor-record-utils.js";
3
+ import { stringifyUnknown } from "./cursor-transcript-utils.js";
3
4
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
4
5
 
5
6
  const CURSOR_AGENT_MESSAGE_PAGE_LIMIT = 8;
@@ -0,0 +1,18 @@
1
+ import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
2
+
3
+ export type CursorAgentsContextExtensionApi = CursorModelLifecycleExtensionApi;
4
+
5
+ export function registerCursorAgentsContextDedup(pi: CursorAgentsContextExtensionApi): void {
6
+ registerCursorModelLifecycle(pi, {
7
+ beforeAgentStart: async (event, ctx) => {
8
+ const { resolveCursorFacingSystemPrompt } = await import("./cursor-agents-context.js");
9
+ const resolved = resolveCursorFacingSystemPrompt(
10
+ event.systemPrompt,
11
+ ctx.model,
12
+ event.systemPromptOptions,
13
+ );
14
+ if (resolved === event.systemPrompt) return undefined;
15
+ return { systemPrompt: resolved };
16
+ },
17
+ });
18
+ }
@@ -1,20 +1,17 @@
1
1
  import type {
2
- BeforeAgentStartEvent,
3
- BeforeAgentStartEventResult,
4
2
  BuildSystemPromptOptions,
5
- ExtensionAPI,
6
3
  ExtensionContext,
7
- ExtensionHandler,
8
4
  } from "@earendil-works/pi-coding-agent";
9
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
10
6
  import { parseEnvBoolean } from "./cursor-env-boolean.js";
11
7
  import { isCursorModel } from "./cursor-model.js";
12
8
  import {
13
- cursorSettingSourcesLoadProjectAgentsRules,
14
- cursorSettingSourcesLoadUserAgentsRules,
9
+ cursorSettingSourcesIncludes,
15
10
  getEffectiveCursorSettingSources,
11
+ resolveCursorSettingSources,
16
12
  } from "./cursor-setting-sources.js";
17
13
  import type { SettingSource } from "@cursor/sdk";
14
+ export { registerCursorAgentsContextDedup, type CursorAgentsContextExtensionApi } from "./cursor-agents-context-registration.js";
18
15
 
19
16
  export const CURSOR_PRESERVE_PI_AGENTS_MD_ENV = "PI_CURSOR_PRESERVE_PI_AGENTS_MD";
20
17
 
@@ -49,18 +46,24 @@ export function getAgentsContextFileBaseName(filePath: string): string {
49
46
  return normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
50
47
  }
51
48
 
49
+ function isPiAgentDirContextFilePath(
50
+ filePath: string,
51
+ fileName: "agents.md" | "claude.md",
52
+ agentDir: string = getAgentDir(),
53
+ ): boolean {
54
+ const normalized = normalizeContextPath(filePath);
55
+ const expectedPath = `${normalizeDirPath(agentDir)}/${fileName}`;
56
+ return normalized.toLowerCase() === expectedPath.toLowerCase();
57
+ }
58
+
52
59
  /** Actual pi agent dir `AGENTS.md` — overlaps Cursor `user` setting source (global agent instructions). */
53
60
  export function isPiAgentDirAgentsMdPath(filePath: string, agentDir: string = getAgentDir()): boolean {
54
- const normalized = normalizeContextPath(filePath);
55
- const agentsMdPath = `${normalizeDirPath(agentDir)}/agents.md`;
56
- return normalized.toLowerCase() === agentsMdPath.toLowerCase();
61
+ return isPiAgentDirContextFilePath(filePath, "agents.md", agentDir);
57
62
  }
58
63
 
59
64
  /** Actual pi agent dir `CLAUDE.md` — kept because Cursor user rules use `~/.claude/CLAUDE.md`. */
60
65
  export function isPiAgentDirClaudeMdPath(filePath: string, agentDir: string = getAgentDir()): boolean {
61
- const normalized = normalizeContextPath(filePath);
62
- const claudeMdPath = `${normalizeDirPath(agentDir)}/claude.md`;
63
- return normalized.toLowerCase() === claudeMdPath.toLowerCase();
66
+ return isPiAgentDirContextFilePath(filePath, "claude.md", agentDir);
64
67
  }
65
68
 
66
69
  /**
@@ -87,9 +90,9 @@ export function shouldRemovePiAgentsContextFile(
87
90
  ): boolean {
88
91
  switch (classifyContextFileOverlap(file.path, agentDir)) {
89
92
  case "cursor-user-agents":
90
- return cursorSettingSourcesLoadUserAgentsRules(settingSources);
93
+ return cursorSettingSourcesIncludes(settingSources, "user");
91
94
  case "cursor-project-rules":
92
- return cursorSettingSourcesLoadProjectAgentsRules(settingSources);
95
+ return cursorSettingSourcesIncludes(settingSources, "project");
93
96
  default:
94
97
  return false;
95
98
  }
@@ -153,24 +156,12 @@ export function resolveCursorFacingSystemPrompt(
153
156
  ): string {
154
157
  if (!systemPromptOptions) return systemPrompt;
155
158
  const contextFiles = systemPromptOptions.contextFiles ?? [];
156
- const settingSources = getEffectiveCursorSettingSources(settingSourcesRaw);
159
+ const settingSources =
160
+ settingSourcesRaw === undefined
161
+ ? getEffectiveCursorSettingSources()
162
+ : resolveCursorSettingSources(settingSourcesRaw);
157
163
  if (!shouldSuppressPiAgentsContext(model, contextFiles, settingSources, agentDir)) {
158
164
  return systemPrompt;
159
165
  }
160
166
  return removePiAgentsContextFromSystemPrompt(systemPrompt, contextFiles, settingSources, agentDir);
161
167
  }
162
-
163
- type CursorAgentsContextExtensionApi = Pick<ExtensionAPI, "on">;
164
-
165
- export function registerCursorAgentsContextDedup(pi: CursorAgentsContextExtensionApi): void {
166
- const handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult> = (event, ctx) => {
167
- const resolved = resolveCursorFacingSystemPrompt(
168
- event.systemPrompt,
169
- ctx.model,
170
- event.systemPromptOptions,
171
- );
172
- if (resolved === event.systemPrompt) return;
173
- return { systemPrompt: resolved };
174
- };
175
- pi.on("before_agent_start", handler);
176
- }
@@ -1,8 +1,10 @@
1
+ import { asRecord } from "./cursor-record-utils.js";
2
+
1
3
  const CURSOR_EDIT_DIFF_FIELD_ORDER = ["diffString", "diff", "unifiedDiff", "patch"] as const;
2
4
 
3
5
  export function resolveCursorEditDiff(source: unknown): string | undefined {
4
- if (!source || typeof source !== "object") return undefined;
5
- const record = source as Record<string, unknown>;
6
+ const record = asRecord(source);
7
+ if (!record) return undefined;
6
8
  for (const key of CURSOR_EDIT_DIFF_FIELD_ORDER) {
7
9
  const value = record[key];
8
10
  if (typeof value === "string" && value.length > 0) return value;
@@ -0,0 +1,22 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { isCursorModel } from "./cursor-model.js";
3
+ import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
4
+ import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
5
+ import type { CursorModelFallbackIssue } from "./model-discovery.js";
6
+
7
+ export type CursorFallbackWarningExtensionApi = CursorModelLifecycleExtensionApi;
8
+
9
+ export function registerCursorFallbackIssueWarning(
10
+ pi: CursorFallbackWarningExtensionApi,
11
+ issue: CursorModelFallbackIssue,
12
+ ): void {
13
+ const warnedSessionScopeKeys = new Set<string>();
14
+
15
+ registerCursorModelLifecycle(pi, (ctx: ExtensionContext) => {
16
+ if (!isCursorModel(ctx.model) || !ctx.hasUI) return;
17
+ const scopeKey = getCursorSessionScopeKey();
18
+ if (warnedSessionScopeKeys.has(scopeKey)) return;
19
+ warnedSessionScopeKeys.add(scopeKey);
20
+ ctx.ui.notify(issue.message, "warning");
21
+ });
22
+ }
@@ -1,4 +1,4 @@
1
- import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME } from "./cursor-tool-names.js";
1
+ import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME, getCursorToolActivityTitle } from "./cursor-tool-presentation-registry.js";
2
2
  import { truncateCursorDisplayLine } from "./cursor-display-text.js";
3
3
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
4
4
  import {
@@ -10,7 +10,8 @@ import {
10
10
  parseCursorReplayToolDetails,
11
11
  resolveIncompleteReplayActivitySourceToolName,
12
12
  } from "./cursor-replay-tool-details.js";
13
- import { truncateArg, type CursorPiToolDisplay } from "./cursor-transcript-utils.js";
13
+ import { asRecord } from "./cursor-record-utils.js";
14
+ import { type CursorPiToolDisplay } from "./cursor-transcript-utils.js";
14
15
  import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
15
16
 
16
17
  export type IncompleteCursorToolDiscardReason = DiscardedIncompleteStartedToolCallReason;
@@ -59,11 +60,6 @@ export function resolveIncompleteCursorToolVisibility(
59
60
  return "emit";
60
61
  }
61
62
 
62
- function buildGenericIncompleteActivityTitle(displayName: string): string {
63
- if (!displayName || displayName === "unknown") return "Cursor tool";
64
- return `Cursor ${truncateArg(displayName)}`;
65
- }
66
-
67
63
  export function formatIncompleteCursorToolReasonText(reason: IncompleteCursorToolDiscardReason): string {
68
64
  switch (reason) {
69
65
  case DISCARDED_INCOMPLETE_TOOL_CALL_REASON:
@@ -79,7 +75,7 @@ export function formatIncompleteCursorToolReasonText(reason: IncompleteCursorToo
79
75
 
80
76
  export function getIncompleteCursorToolActivityTitle(toolCall: unknown): string {
81
77
  const visibility = classifyCursorToolVisibility(toolCall);
82
- return visibility.incompleteTitle ?? buildGenericIncompleteActivityTitle(visibility.displayName);
78
+ return visibility.incompleteTitle ?? getCursorToolActivityTitle(visibility.displayName);
83
79
  }
84
80
 
85
81
  export function buildIncompleteCursorToolDisplay(
@@ -124,8 +120,7 @@ export function formatIncompleteCursorToolTrace(display: CursorPiToolDisplay): s
124
120
  formatIncompleteCursorToolReasonText(DISCARDED_INCOMPLETE_TOOL_CALL_REASON);
125
121
  return `${truncateCursorDisplayLine(parsed.title)}: ${truncateCursorDisplayLine(summary)}\n`;
126
122
  }
127
- const details = display.result.details;
128
- const detailRecord = details && typeof details === "object" ? (details as Record<string, unknown>) : undefined;
123
+ const detailRecord = asRecord(display.result.details);
129
124
  const argsRecord = display.args;
130
125
  const title =
131
126
  (typeof detailRecord?.title === "string" && detailRecord.title.trim()) ||
@@ -7,7 +7,7 @@ import {
7
7
  type CursorLiveRunAccountingState,
8
8
  type CursorLiveToolResultConsumption,
9
9
  } from "./cursor-live-run-accounting.js";
10
- import type { CursorNativeToolDisplayItem } from "./cursor-native-tool-display.js";
10
+ import type { CursorNativeToolDisplayItem } from "./cursor-native-tool-display-state.js";
11
11
  import type { CursorPiBridgeToolRequest, CursorPiToolBridgeRun } from "./cursor-pi-tool-bridge.js";
12
12
  import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
13
13
  import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
@@ -151,8 +151,6 @@ export function restoreCursorMcpToolTimeoutOverride(): void {
151
151
  installedConnectTimeoutMs = DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
152
152
  }
153
153
 
154
- export const restoreCursorMcpToolTimeoutOverrideForTests = restoreCursorMcpToolTimeoutOverride;
155
-
156
154
  export const cursorMcpToolTimeoutOverrideDefaults = {
157
155
  cursorSdkDefaultTimeoutMs: CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS,
158
156
  defaultOverrideTimeoutMs: DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS,
@@ -0,0 +1,72 @@
1
+ import type {
2
+ BeforeAgentStartEvent,
3
+ BeforeAgentStartEventResult,
4
+ ExtensionContext,
5
+ ExtensionHandler,
6
+ SessionStartEvent,
7
+ TurnStartEvent,
8
+ } from "@earendil-works/pi-coding-agent";
9
+
10
+ export type CursorModelLifecycleContext = ExtensionContext;
11
+
12
+ type CursorModelSelectEvent = { model: ExtensionContext["model"] };
13
+
14
+ type CursorModelLifecycleSyncHandler = (ctx: CursorModelLifecycleContext) => Promise<void> | void;
15
+ type CursorModelSessionStartHandler = ExtensionHandler<SessionStartEvent>;
16
+ type CursorModelSelectHandler = (event: CursorModelSelectEvent, ctx: CursorModelLifecycleContext) => Promise<void> | void;
17
+ type CursorModelTurnStartHandler = ExtensionHandler<TurnStartEvent>;
18
+ type CursorModelBeforeAgentStartHandler = ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>;
19
+
20
+ export interface CursorModelLifecycleExtensionApi {
21
+ on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
22
+ on(event: "before_agent_start", handler: CursorModelBeforeAgentStartHandler): void;
23
+ on(event: "model_select", handler: (event: CursorModelSelectEvent, ctx: ExtensionContext) => Promise<void> | void): void;
24
+ on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
25
+ }
26
+
27
+ export interface CursorModelLifecycleHandlers {
28
+ sessionStart?: CursorModelSessionStartHandler;
29
+ modelSelect?: CursorModelSelectHandler;
30
+ turnStart?: CursorModelTurnStartHandler;
31
+ sync?: CursorModelLifecycleSyncHandler;
32
+ beforeAgentStart?: CursorModelBeforeAgentStartHandler;
33
+ }
34
+
35
+ function normalizeLifecycleHandlers(
36
+ handlerOrHandlers: CursorModelLifecycleSyncHandler | CursorModelLifecycleHandlers,
37
+ ): CursorModelLifecycleHandlers {
38
+ return typeof handlerOrHandlers === "function" ? { sync: handlerOrHandlers } : handlerOrHandlers;
39
+ }
40
+
41
+ export function registerCursorModelLifecycle(
42
+ pi: CursorModelLifecycleExtensionApi,
43
+ handlerOrHandlers: CursorModelLifecycleSyncHandler | CursorModelLifecycleHandlers,
44
+ ): void {
45
+ const handlers = normalizeLifecycleHandlers(handlerOrHandlers);
46
+ const sync = handlers.sync;
47
+ if (handlers.sessionStart || sync) {
48
+ pi.on("session_start", async (event, ctx) => {
49
+ await handlers.sessionStart?.(event, ctx);
50
+ await sync?.(ctx);
51
+ });
52
+ }
53
+ if (handlers.modelSelect || sync) {
54
+ pi.on("model_select", async (event, ctx) => {
55
+ const effectiveCtx = { ...ctx, model: event.model };
56
+ await handlers.modelSelect?.(event, effectiveCtx);
57
+ await sync?.(effectiveCtx);
58
+ });
59
+ }
60
+ if (handlers.turnStart || sync) {
61
+ pi.on("turn_start", async (event, ctx) => {
62
+ await handlers.turnStart?.(event, ctx);
63
+ await sync?.(ctx);
64
+ });
65
+ }
66
+ if (handlers.beforeAgentStart || sync) {
67
+ pi.on("before_agent_start", async (event, ctx) => {
68
+ await sync?.(ctx);
69
+ return await handlers.beforeAgentStart?.(event, ctx);
70
+ });
71
+ }
72
+ }
@@ -1,4 +1,4 @@
1
- import { canRenderCursorToolNatively } from "./cursor-native-tool-display.js";
1
+ import { canRenderCursorToolNatively } from "./cursor-native-tool-display-state.js";
2
2
  import { getActiveContextToolNames } from "./cursor-context-tools.js";
3
3
  import type { Context } from "@earendil-works/pi-ai";
4
4
 
@@ -1,4 +1,4 @@
1
- import type { CursorPiToolDisplay } from "./cursor-tool-transcript.js";
1
+ import type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
2
2
  import { asRecord } from "./cursor-record-utils.js";
3
3
  import { truncateCursorDisplayLine } from "./cursor-display-text.js";
4
4
 
@@ -1,12 +1,12 @@
1
- import type { BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, ExtensionHandler, SessionStartEvent, TurnStartEvent } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES,
4
4
  isNativeCursorToolName,
5
5
  NATIVE_CURSOR_TOOL_NAMES,
6
- registerNativeCursorTool,
7
6
  type NativeCursorToolName,
8
- } from "./cursor-native-tool-display-tools.js";
7
+ } from "./cursor-native-tool-names.js";
9
8
  import { isCursorModel } from "./cursor-model.js";
9
+ import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
10
10
  import {
11
11
  isCursorNativeToolDisplayRequested,
12
12
  isCursorNativeToolRegistrationRequested,
@@ -15,7 +15,7 @@ import {
15
15
  registeredNativeToolNames,
16
16
  skippedNativeToolNames,
17
17
  } from "./cursor-native-tool-display-state.js";
18
- import { isCursorReplayToolName } from "./cursor-tool-names.js";
18
+ import { isCursorReplayToolName } from "./cursor-tool-presentation-registry.js";
19
19
 
20
20
  export const CURSOR_CORE_PI_REPLAY_TOOL_NAMES = ["read", "bash", "edit", "write"] as const;
21
21
  const CORE_PI_TOOL_NAMES = new Set<string>(CURSOR_CORE_PI_REPLAY_TOOL_NAMES);
@@ -27,12 +27,7 @@ function isCursorCorePiReplayToolName(toolName: string): toolName is (typeof CUR
27
27
  type CursorNativeToolActivationApi = Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">;
28
28
  type CursorNativeToolRegistryApi = CursorNativeToolActivationApi & Pick<ExtensionAPI, "getAllTools" | "registerTool">;
29
29
 
30
- export interface CursorNativeToolDisplayExtensionApi extends CursorNativeToolRegistryApi {
31
- on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
32
- on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent>): void;
33
- on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
34
- on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
35
- }
30
+ export interface CursorNativeToolDisplayExtensionApi extends CursorNativeToolRegistryApi, CursorModelLifecycleExtensionApi {}
36
31
 
37
32
  function hasNonBuiltinTool(pi: Pick<ExtensionAPI, "getAllTools">, toolName: NativeCursorToolName): boolean {
38
33
  const existingTool = pi.getAllTools().find((tool) => tool.name === toolName);
@@ -43,11 +38,12 @@ type NativeRegistrationContext = Pick<ExtensionContext, "mode" | "model"> & {
43
38
  ui: Pick<ExtensionContext["ui"], "notify">;
44
39
  };
45
40
 
46
- function registerNativeCursorToolsFromSet(
41
+ async function registerNativeCursorToolsFromSet(
47
42
  pi: CursorNativeToolRegistryApi,
48
43
  toolNames: readonly NativeCursorToolName[],
49
- ): NativeCursorToolName[] {
44
+ ): Promise<NativeCursorToolName[]> {
50
45
  const newlySkippedToolNames: NativeCursorToolName[] = [];
46
+ let registerNativeCursorTool: ((pi: CursorNativeToolRegistryApi, toolName: NativeCursorToolName) => void) | undefined;
51
47
  for (const toolName of toolNames) {
52
48
  if (registeredNativeToolNames.has(toolName) || skippedNativeToolNames.has(toolName)) continue;
53
49
  if (hasNonBuiltinTool(pi, toolName)) {
@@ -55,6 +51,7 @@ function registerNativeCursorToolsFromSet(
55
51
  newlySkippedToolNames.push(toolName);
56
52
  continue;
57
53
  }
54
+ registerNativeCursorTool ??= (await import("./cursor-native-tool-display-tools.js")).registerNativeCursorTool;
58
55
  registerNativeCursorTool(pi, toolName);
59
56
  registeredNativeToolNames.add(toolName);
60
57
  }
@@ -97,7 +94,7 @@ export function syncRegisteredNativeCursorToolsForModel(
97
94
  if (changed) pi.setActiveTools([...activeToolNames]);
98
95
  }
99
96
 
100
- function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
97
+ async function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): Promise<void> {
101
98
  if (!isCursorNativeToolRegistrationRequested()) {
102
99
  registeredNativeToolNames.clear();
103
100
  skippedNativeToolNames.clear();
@@ -107,31 +104,22 @@ function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryA
107
104
 
108
105
  const nonCoreToolNames = NATIVE_CURSOR_TOOL_NAMES.filter((toolName) => !isCursorCorePiReplayToolName(toolName));
109
106
  const skippedToolNames = [
110
- ...registerNativeCursorToolsFromSet(pi, nonCoreToolNames),
111
- ...registerNativeCursorToolsFromSet(pi, CURSOR_CORE_PI_REPLAY_TOOL_NAMES),
107
+ ...(await registerNativeCursorToolsFromSet(pi, nonCoreToolNames)),
108
+ ...(await registerNativeCursorToolsFromSet(pi, CURSOR_CORE_PI_REPLAY_TOOL_NAMES)),
112
109
  ];
113
110
  notifySkippedNativeCursorToolsIfNeeded(ctx, skippedToolNames);
114
111
  }
115
112
 
116
- function ensureThenSyncNativeCursorToolsForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
113
+ async function ensureThenSyncNativeCursorToolsForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): Promise<void> {
117
114
  if (isCursorModel(ctx.model) && !hasAttemptedNativeCursorToolRegistration()) {
118
- ensureNativeCursorToolsRegisteredForModel(pi, ctx);
115
+ await ensureNativeCursorToolsRegisteredForModel(pi, ctx);
119
116
  }
120
117
  syncRegisteredNativeCursorToolsForModel(pi, ctx.model);
121
118
  }
122
119
 
123
120
  export function registerCursorNativeToolDisplay(pi: CursorNativeToolDisplayExtensionApi): void {
124
- pi.on("session_start", (_event, ctx) => {
125
- ensureThenSyncNativeCursorToolsForModel(pi, ctx);
126
- });
127
- pi.on("before_agent_start", (_event, ctx) => {
128
- ensureThenSyncNativeCursorToolsForModel(pi, ctx);
129
- });
130
- pi.on("turn_start", (_event, ctx) => {
131
- ensureThenSyncNativeCursorToolsForModel(pi, ctx);
132
- });
133
- pi.on("model_select", (event, ctx) => {
134
- ensureThenSyncNativeCursorToolsForModel(pi, { ...ctx, model: event.model });
121
+ registerCursorModelLifecycle(pi, async (ctx) => {
122
+ await ensureThenSyncNativeCursorToolsForModel(pi, ctx);
135
123
  });
136
124
  }
137
125
 
@@ -1,9 +1,10 @@
1
1
  import { readFileSync, statSync } from "node:fs";
2
- import { basename, extname } from "node:path";
2
+ import { basename } from "node:path";
3
3
  import { getLanguageFromPath, highlightCode, type ToolDefinition } from "@earendil-works/pi-coding-agent";
4
4
  import { Image, Text, type Component } from "@earendil-works/pi-tui";
5
5
  import { Type } from "typebox";
6
6
  import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
7
+ import { inferImageMimeType } from "./cursor-tool-result-display-readers.js";
7
8
  import { LOCAL_READ_PREVIEW_NOTICE, isLocalReadPreviewContent } from "./cursor-transcript-utils.js";
8
9
  import {
9
10
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
@@ -17,7 +18,6 @@ import {
17
18
  type CursorReplayActivityDetails,
18
19
  type CursorReplayToolDetails,
19
20
  type CursorReplayNativeWriteDetails,
20
- isCursorReplayNativeEditDetails,
21
21
  isCursorReplayGenerateImageDetails,
22
22
  isCursorReplayActivityDetails,
23
23
  parseCursorReplayToolDetails,
@@ -49,22 +49,6 @@ type CursorReplayRenderCall = NonNullable<ToolDefinition<typeof cursorReplayTool
49
49
  type CursorReplayRenderResult = NonNullable<ToolDefinition<typeof cursorReplayToolSchema, unknown>["renderResult"]>;
50
50
  export type CursorReplayRenderTheme = Parameters<CursorReplayRenderCall>[1];
51
51
 
52
- function inferImageMimeTypeFromPath(path: string | undefined): string | undefined {
53
- switch (extname(path ?? "").toLowerCase()) {
54
- case ".png":
55
- return "image/png";
56
- case ".jpg":
57
- case ".jpeg":
58
- return "image/jpeg";
59
- case ".gif":
60
- return "image/gif";
61
- case ".webp":
62
- return "image/webp";
63
- default:
64
- return undefined;
65
- }
66
- }
67
-
68
52
  function readImageFileForReplay(path: string | undefined): string | undefined {
69
53
  if (!path) return undefined;
70
54
  try {
@@ -464,7 +448,7 @@ function renderExpandableCursorReplayResult(
464
448
  }
465
449
  if (details.imagePath && !isError && context.showImages) {
466
450
  const imageData = readImageFileForReplay(details.imagePath);
467
- const mimeType = details.imageMimeType ?? inferImageMimeTypeFromPath(details.imagePath);
451
+ const mimeType = details.imageMimeType ?? inferImageMimeType(details.imagePath);
468
452
  if (imageData && mimeType) return buildImageReplayComponent(rendered, imageData, mimeType, basename(details.imagePath ?? "generated-image"), theme);
469
453
  }
470
454
  return new Text(rendered, 0, 0);
@@ -485,7 +469,6 @@ function renderCursorReplayEditResult(
485
469
  function renderCursorReplayWriteResult(
486
470
  details: CursorReplayNativeWriteDetails,
487
471
  result: Parameters<CursorReplayRenderResult>[0],
488
- options: Parameters<CursorReplayRenderResult>[1],
489
472
  theme: Parameters<CursorReplayRenderResult>[2],
490
473
  ): Component {
491
474
  const text = firstContentText(result);
@@ -532,7 +515,7 @@ function renderCursorReplayDetails(
532
515
  case "nativeEdit":
533
516
  return renderCursorReplayEditResult(details, options, theme);
534
517
  case "nativeWrite":
535
- return renderCursorReplayWriteResult(details, result, options, theme);
518
+ return renderCursorReplayWriteResult(details, result, theme);
536
519
  case "generateImage":
537
520
  return renderCursorGenerateImageResult(details, result, options, theme, context, isError);
538
521
  case "activity":
@@ -1,4 +1,4 @@
1
- import type { CursorPiToolDisplay } from "./cursor-tool-transcript.js";
1
+ import type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
2
2
  import { parseOptionalEnvBoolean } from "./cursor-env-boolean.js";
3
3
 
4
4
  export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {