pi-cursor-sdk 0.1.37 → 0.1.39

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 (77) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +2 -2
  3. package/docs/cursor-model-ux-spec.md +1 -1
  4. package/docs/cursor-native-tool-replay.md +5 -5
  5. package/package.json +1 -1
  6. package/scripts/platform-smoke/card-detect.mjs +1 -1
  7. package/src/context-window-cache.ts +10 -14
  8. package/src/context.ts +1 -1
  9. package/src/cursor-agent-message-web-tools.ts +2 -1
  10. package/src/cursor-agents-context-registration.ts +18 -0
  11. package/src/cursor-agents-context.ts +21 -30
  12. package/src/cursor-edit-diff.ts +4 -2
  13. package/src/cursor-fallback-warning.ts +22 -0
  14. package/src/cursor-incomplete-tool-visibility.ts +5 -10
  15. package/src/cursor-live-run-coordinator.ts +1 -1
  16. package/src/cursor-mcp-timeout-override.ts +0 -2
  17. package/src/cursor-model-lifecycle.ts +72 -0
  18. package/src/cursor-native-replay-routing.ts +1 -1
  19. package/src/cursor-native-replay-trace.ts +1 -1
  20. package/src/cursor-native-tool-display-registration.ts +16 -28
  21. package/src/cursor-native-tool-display-replay.ts +4 -21
  22. package/src/cursor-native-tool-display-state.ts +1 -1
  23. package/src/cursor-native-tool-display-tools.ts +10 -17
  24. package/src/cursor-native-tool-names.ts +16 -0
  25. package/src/cursor-pi-tool-bridge-env.ts +12 -0
  26. package/src/cursor-pi-tool-bridge-mcp.ts +16 -21
  27. package/src/cursor-pi-tool-bridge-run.ts +5 -5
  28. package/src/cursor-pi-tool-bridge-server.ts +8 -3
  29. package/src/cursor-pi-tool-bridge-snapshot.ts +7 -13
  30. package/src/cursor-pi-tool-bridge.ts +7 -7
  31. package/src/cursor-provider-errors.ts +11 -4
  32. package/src/cursor-provider-lazy.ts +51 -0
  33. package/src/cursor-provider-live-run-drain.ts +1 -1
  34. package/src/cursor-provider-run-finalizer.ts +5 -5
  35. package/src/cursor-provider-run-outcome.ts +0 -1
  36. package/src/cursor-provider-turn-coordinator.ts +16 -6
  37. package/src/cursor-provider-turn-display-router.ts +5 -1
  38. package/src/cursor-provider-turn-emit.ts +1 -1
  39. package/src/cursor-provider-turn-lifecycle-emitter.ts +1 -5
  40. package/src/cursor-provider-turn-prepare.ts +13 -9
  41. package/src/cursor-provider-turn-runner.ts +3 -11
  42. package/src/cursor-provider-turn-sdk-normalizer.ts +28 -5
  43. package/src/cursor-provider-turn-send.ts +7 -2
  44. package/src/cursor-provider-turn-shell-output.ts +38 -3
  45. package/src/cursor-provider-turn-types.ts +1 -3
  46. package/src/cursor-provider.ts +3 -2
  47. package/src/cursor-question-tool.ts +5 -18
  48. package/src/cursor-record-utils.ts +42 -0
  49. package/src/cursor-replay-activity-builders.ts +16 -122
  50. package/src/cursor-replay-tool-details.ts +52 -80
  51. package/src/cursor-sdk-event-debug.ts +6 -6
  52. package/src/cursor-sensitive-text.ts +4 -4
  53. package/src/cursor-session-agent-lifecycle.ts +47 -0
  54. package/src/cursor-session-agent.ts +9 -47
  55. package/src/cursor-session-scope.ts +23 -4
  56. package/src/cursor-setting-sources.ts +8 -8
  57. package/src/cursor-skill-tool.ts +25 -32
  58. package/src/cursor-state.ts +66 -45
  59. package/src/cursor-tool-lifecycle.ts +22 -10
  60. package/src/cursor-tool-presentation-registry.ts +27 -18
  61. package/src/cursor-tool-result-display-readers.ts +185 -0
  62. package/src/cursor-tool-transcript.ts +17 -33
  63. package/src/cursor-tool-visibility.ts +9 -1
  64. package/src/cursor-transcript-tool-formatters.ts +23 -172
  65. package/src/cursor-transcript-tool-specs.ts +16 -41
  66. package/src/cursor-transcript-utils.ts +2 -34
  67. package/src/cursor-usage-accounting.ts +0 -6
  68. package/src/cursor-web-tool-activity.ts +4 -12
  69. package/src/cursor-web-tool-args.ts +1 -9
  70. package/src/index.ts +15 -16
  71. package/src/model-discovery.ts +5 -4
  72. package/src/model-list-cache.ts +37 -38
  73. package/src/cursor-native-tool-display.ts +0 -10
  74. package/src/cursor-provider-turn-api-key.ts +0 -1
  75. package/src/cursor-provider-turn-message-offset.ts +0 -15
  76. package/src/cursor-session-cwd.ts +0 -28
  77. package/src/cursor-tool-names.ts +0 -9
package/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.39 - 2026-06-08
6
+
7
+ ### Fixed
8
+
9
+ - Surface Cursor shell command starts with scrubbed command previews, including path-bearing commands, and stream bounded `shell-output-delta` stdout/stderr progress before completion so users do not stare at only pi's generic `Working...` state.
10
+ - Mark generic Cursor SDK run failures and Cursor SDK network failures with pi-native retry classifier phrases so pi's existing auto-retry/backoff flow can recover transient failures automatically instead of requiring a manual follow-up message.
11
+
12
+ ## 0.1.38 - 2026-06-08
13
+
14
+ ### Added
15
+
16
+ - 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.
17
+ - Add a canonical Cursor model lifecycle sync helper for session start, before-agent-start, model selection, and turn start registration paths.
18
+ - Add lazy Cursor provider registration so extension startup can register models and commands without importing the Cursor SDK runtime until the provider is invoked.
19
+ - Add shared Cursor native tool-name and pi-tool-bridge environment helpers for provider/runtime registration code.
20
+
21
+ ### Changed
22
+
23
+ - Centralize Cursor tool presentation ownership in the typed presentation registry, including labels, aliases, lifecycle titles, replay metadata, side-effect policies, and web-tool classification.
24
+ - Consolidate Cursor session cwd, session file/id, generation, and scope-key handling in `cursor-session-scope`; remove the older cwd/message-offset helper split.
25
+ - Simplify Cursor session-agent lifecycle invalidation on model select, compaction preparation, tree navigation, shutdown, and scope changes.
26
+ - 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.
27
+ - Keep Cursor agents-context dedup and fallback-catalog warning registration model-scoped through the shared lifecycle helper.
28
+ - Keep edit/write replay previews on the shared structured diff/file preview renderers while retaining SDK expanded-text fallback behavior.
29
+ - Update maintainer docs and repo map entries for the new ownership boundaries.
30
+
31
+ ### Fixed
32
+
33
+ - 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.
34
+ - Keep Cursor agents-context dedup registration in a tracked module so clean package builds resolve `src/index.ts` imports.
35
+ - Accept Windows-rendered absolute `README.md` paths in platform-smoke grep-card detection without weakening prompt false-positive checks.
36
+ - Preserve Cursor skill activation and question-tool registration through lazy provider/runtime import boundaries.
37
+ - Preserve fast local discovery incomplete-tool suppression while still surfacing aborts, SDK failures, and no-text incomplete runs.
38
+
5
39
  ## 0.1.37 - 2026-06-06
6
40
 
7
41
  ### Changed
package/README.md CHANGED
@@ -361,11 +361,11 @@ Actual Cursor runs still need a key from `/login`, `CURSOR_API_KEY`, or `--api-k
361
361
 
362
362
  You may be seeing fallback startup models or a missing/invalid Cursor SDK API key. Cursor Agent CLI/Desktop login is not reused by this extension. In interactive pi, run `/login`, choose `Use an API key`, choose `Cursor`, paste the key, then run `/cursor-refresh-models`.
363
363
 
364
- When a Cursor run fails after auth is configured, pi now surfaces scrubbed provider detail instead of only `Cursor SDK run failed`. Generic SDK failures include safe run metadata such as model id, a short run id prefix, and duration when available. Check the red toast or assistant error message for that detail before retrying.
364
+ When a Cursor run fails after auth is configured, pi now surfaces scrubbed provider detail instead of only `Cursor SDK run failed`. Generic SDK failures include safe run metadata such as model id, a short run id prefix, and duration when available, and are phrased as pi retryable provider errors so automatic retry/backoff can recover transient SDK failures.
365
365
 
366
366
  Aborted runs now include a likely cause when determinable, for example `Cancelled: prompt interrupted.` for user cancel or `Cancelled: Cursor SDK run was cancelled.` for SDK-side cancellation.
367
367
 
368
- Network failures from the Cursor SDK connect layer (for example `ConnectError: read ETIMEDOUT` or `ConnectError: [aborted] read ECONNRESET`) surface as a scrubbed retry hint instead of crashing pi. Check your connection and retry; persistent failures may indicate a transient Cursor service or network issue.
368
+ Network failures from the Cursor SDK connect layer (for example `ConnectError: read ETIMEDOUT` or `ConnectError: [aborted] read ECONNRESET`) surface as scrubbed `Network error` messages instead of crashing pi, matching pi's native auto-retry classifier. Persistent failures may indicate a transient Cursor service or network issue.
369
369
 
370
370
  You can also restart pi with a key in the same shell or launcher that starts pi:
371
371
 
@@ -29,7 +29,7 @@ Current implementation notes:
29
29
  - Cursor SDK MCP tool calls use a guarded timeout override because installed `@cursor/sdk` 1.0.17 has a 60-second MCP request default with no public per-server timeout option. The extension extends the verified Cursor SDK MCP `callTool` timeout path to 3600 seconds by default and shortens the verified first-send MCP initialize/listTools timeout paths to 10 seconds by default so unavailable configured MCP servers do not block the first reply for a full minute; unknown MCP protocol timeout stacks keep the SDK default. Users can override tool-call timeouts with `PI_CURSOR_MCP_TOOL_TIMEOUT_MS` or `PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS`, and initialize/listTools timeouts with `PI_CURSOR_MCP_CONNECT_TIMEOUT_MS` or `PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS`.
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
- - 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.
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 also shown as bounded live progress while one shell call is active and used as display-only fallback for empty successful shell completions, while 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
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.
@@ -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
  | --- | --- |
@@ -169,7 +169,7 @@ Most Cursor tool visibility is completion-based: the completed replay card (or b
169
169
  Lifecycle rules:
170
170
 
171
171
  - Eligible tools include `task`, `shell`, `mcp`, `generateImage`, `recordScreen`, `semSearch`, web search/fetch activity, and plan/todo activity. Fast local tools such as `read`, `grep`, and `glob` do not get lifecycle lines in normal cases.
172
- - Lifecycle text is emitted as a single bounded, scrubbed thinking line such as `Cursor MCP: external_search` or `Cursor shell: shell`. Shell pending labels intentionally omit command text; the completed replay card remains the source of truth for recorded shell activity. Lifecycle lines are not separate permanent replay cards and do not rerun tools.
172
+ - Lifecycle text is emitted as a single bounded, scrubbed thinking line such as `Cursor MCP: external_search` or `Cursor shell: npm test`. Shell pending labels show a scrubbed/truncated command preview, matching pi's native bash UX; the completed replay card remains the source of truth for recorded shell results. Lifecycle lines are not separate permanent replay cards and do not rerun tools.
173
173
  - A short defer window coalesces fast start+complete pairs: if a tool completes before the defer elapses, only the completed replay card/trace is shown.
174
174
  - pi bridge MCP calls (`pi__*`) are excluded because pi already shows the real pi tool execution path.
175
175
  - Implementation: `src/cursor-tool-lifecycle.ts` (eligibility/labels) and `src/cursor-provider-turn-coordinator.ts` (defer, emit, bridge exclusion).
@@ -180,7 +180,7 @@ As Cursor SDK tool completions arrive, the extension mirrors native Codex orderi
180
180
 
181
181
  Bridged pi tool calls follow the same visible pi `toolUse` turn shape, but they are real pi tool executions rather than replayed Cursor results. Split-run usage accounting keeps Cursor SDK internal counters out of pi usage: each live Cursor prompt is counted once, replay/bridge tool-call turns include visible assistant activity in output estimates, consumed tool results are counted once as input on the following assistant turn, and `usage.totalTokens` remains the replayable Cursor prompt/context estimate.
182
182
 
183
- For shell replay, completed `stdout` / `stderr` remain the primary source. If a successful completed shell result is empty and Cursor emitted unambiguous `shell-output-delta` data while exactly one shell call was active, the replay card uses that delta as display-only fallback data. Overlapping shell calls make delta attribution ambiguous, so those fallback deltas are dropped rather than guessed. `(no output)` is kept only when no completed output or safe delta fallback is available.
183
+ For shell replay, completed `stdout` / `stderr` remain the primary source. While exactly one shell call is active, the provider also emits a bounded scrubbed preview of the first few `shell-output-delta` stdout/stderr chunks so long-running commands show visible progress before completion. If a successful completed shell result is empty, the replay card uses unambiguous buffered delta data as display-only fallback data. Overlapping shell calls make delta attribution ambiguous, so those fallback/progress deltas are dropped rather than guessed. `(no output)` is kept only when no completed output or safe delta fallback is available.
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
 
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.39",
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