pi-cursor-sdk 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.1.8 - 2026-05-14
4
+
5
+ ### Changed
6
+
7
+ - Update the verified dependency baseline to `@cursor/sdk` 1.0.13 and Vitest 4.1.6.
8
+ - Register latest-style Cursor SDK model aliases returned by `Cursor.models.list()` as pi-selectable Cursor model IDs, including context-qualified alias variants where applicable.
9
+ - Clarify Max Mode behavior against current Cursor SDK docs: Cursor may enable required Max Mode automatically, but the extension still only advertises catalog-exposed context variants.
10
+
11
+ ## 0.1.7 - 2026-05-10
12
+
13
+ ### Fixed
14
+
15
+ - Preserve Cursor post-tool thinking and text that arrive before a native replay tool-use turn closes.
16
+ - Count prompt input only once when one Cursor SDK run is split across multiple native replay turns.
17
+ - Tighten native replay registration tests and documentation around registration opt-out behavior.
4
18
 
5
19
  ## 0.1.6 - 2026-05-10
6
20
 
package/README.md CHANGED
@@ -137,6 +137,7 @@ How to read model IDs:
137
137
  - `cursor/...` is the Cursor provider registered by this extension
138
138
  - `@1m`, `@272k`, and `@300k` are context-window variants
139
139
  - `:medium`, `:high`, and `:xhigh` are pi thinking-level suffixes for models where the Cursor SDK exposes a pi-controllable thinking parameter
140
+ - latest-style Cursor aliases returned by `Cursor.models.list()` are registered too, using the same context suffixes when the target model has context variants
140
141
 
141
142
  Examples with pi thinking controls:
142
143
 
@@ -146,7 +147,7 @@ pi --model cursor/gpt-5.5@272k:xhigh
146
147
  pi --model cursor/gpt-5.5@1m --thinking medium
147
148
  ```
148
149
 
149
- Cursor-only parameters are not encoded into pi model IDs. Cursor `context` becomes a pi-visible model variant because it changes pi's native `contextWindow`; Cursor `fast` is extension state, not model identity.
150
+ Cursor-only parameters are not encoded into pi model IDs. Cursor `context` becomes a pi-visible model variant because it changes pi's native `contextWindow`; Cursor `fast` is extension state, not model identity. Alias model IDs still share Cursor-only state, such as fast defaults, with their underlying Cursor base model.
150
151
 
151
152
  ## Thinking support
152
153
 
@@ -214,7 +215,7 @@ Fallback models are a conservative startup model list. Actual Cursor runs still
214
215
  - **Pi tool schemas are not passed through to Cursor.** This extension is a Cursor provider, not a bridge that forwards pi's tool system into Cursor.
215
216
  - **One fresh Cursor agent is created per provider call.** Cursor agent state is not reused between pi provider calls.
216
217
  - **Ambient Cursor setting/rule layers are not loaded by default.** The current Cursor SDK writes setting/rule loading logs directly to terminal output, which corrupts pi's TUI, so the extension leaves those layers out.
217
- - **Max Mode is not exposed for these local runs.** The extension only advertises exact context windows supported by the SDK path it uses.
218
+ - **Max Mode is not a manual pi variant.** Cursor's SDK may enable Max Mode automatically for models that require it. This extension only advertises exact context-window variants that the SDK catalog exposes and otherwise uses conservative SDK-derived default/non-Max context windows.
218
219
  - **Output token limits are conservative.** Cursor SDK model metadata does not currently expose output token limits directly.
219
220
  - **Token usage is approximate in pi.** Cursor SDK usage events include internal agent/tool/cache work, so the extension reports an approximate replayable pi prompt/output size for context display and compaction decisions.
220
221
 
@@ -17,10 +17,11 @@ Current implementation notes:
17
17
  - Cursor auth uses pi-native API-key resolution for provider `cursor`: CLI `--api-key`, stored `~/.pi/agent/auth.json` API key from `/login`, then `CURSOR_API_KEY`. The extension config file stores only non-secret Cursor-only state such as fast defaults.
18
18
  - Local agents do not pass `settingSources` by default because the current Cursor SDK writes setting/rule loading INFO logs directly to terminal output, which corrupts pi's TUI.
19
19
  - Cursor SDK models are treated as thinking-capable even when pi reports `thinking=no`; that pi column only means the SDK did not expose a pi-controllable thinking parameter for that model.
20
- - Cursor-side thinking remains visible. Cursor internal tool activity is recorded from SDK events and scrubbed. In interactive TTY sessions, supported completed `read`, `bash`, and `ls` activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so the TUI can show native green cards without forcing Cursor to call pi tools or rerunning Cursor's reads/shell commands. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. When these native cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK completions arrive: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later Cursor tool batches as further `toolUse` turns, then Cursor's final assistant answer. Non-interactive runs keep bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when native tool replay is not active.
21
- - Cursor SDK usage events report cumulative internal agent/tool/cache work, not the replayable pi prompt context. The extension reports approximate prompt/output usage for pi context display and compaction decisions instead of copying raw Cursor SDK usage.
20
+ - Cursor-side thinking remains visible. Cursor internal tool activity is recorded from SDK events and scrubbed. In interactive TTY sessions, supported completed `read`, `bash`, and `ls` activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so the TUI can show native green cards without forcing Cursor to call pi tools or rerunning Cursor's reads/shell commands. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. `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 these native cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK completions arrive: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later Cursor tool batches as further `toolUse` turns, then Cursor's final assistant answer. Non-interactive runs keep bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when native tool replay is not active.
21
+ - Cursor SDK usage events report cumulative internal agent/tool/cache work, not the replayable pi prompt context. The extension reports approximate prompt/output usage for pi context display and compaction decisions instead of copying raw Cursor SDK usage. When native replay splits one Cursor SDK run into multiple pi turns, prompt input is counted once for the run; later synthetic replay turns report `input: 0` and only their own output estimate.
22
22
  - For models without a catalog `context` parameter, context windows are not hardcoded. The extension ships a bundled SDK-derived default/non-Max cache generated from `createAgentPlatform().checkpointStore.loadLatest(agentId).tokenDetails.maxTokens`. Successful runs can update a local override cache, but model discovery does not probe models at startup.
23
- - Max Mode context windows are distinct from default/non-Max context windows. `@cursor/sdk` 1.0.12 exposes internal protobuf fields named `maxMode`/`max_mode`, but the public `ModelSelection` type and the local executor path do not pass a Max Mode selector for local agent runs. Do not advertise Max Mode context windows unless the SDK catalog exposes an exact parameter/variant or the SDK public API adds a Max Mode selector that the extension actually sends.
23
+ - Max Mode context windows are distinct from default/non-Max context windows. `@cursor/sdk` 1.0.13 documentation says the SDK may enable Max Mode automatically when a selected model requires it, but the public local-agent `ModelSelection` path still does not expose a manual Max Mode selector. Do not advertise Max Mode context windows unless the SDK catalog exposes an exact parameter/variant or the SDK public API adds a Max Mode selector that the extension actually sends.
24
+ - `@cursor/sdk` 1.0.13 adds latest-style `ModelListItem.aliases`. The extension registers those aliases as pi model IDs (with the same context suffixes when applicable) and sends the alias back in `ModelSelection.id`, while sharing Cursor-only state such as fast defaults with the underlying catalog `id`.
24
25
 
25
26
  ## Goal
26
27
 
@@ -69,6 +70,7 @@ Users can persist the stored key through `/login` -> `Use an API key` -> `Cursor
69
70
  For each model, use:
70
71
 
71
72
  - `model.id`
73
+ - `model.aliases`
72
74
  - `model.displayName`
73
75
  - `model.parameters`
74
76
  - `model.variants`
@@ -135,8 +137,8 @@ Register a `cursor` provider with `pi.registerProvider()`.
135
137
 
136
138
  Rules:
137
139
 
138
- - Register one pi model for each Cursor base model when there is no Cursor `context` parameter.
139
- - Register one pi model per Cursor `context` value when the model exposes a `context` parameter.
140
+ - Register one pi model for each Cursor base model and SDK alias when there is no Cursor `context` parameter.
141
+ - Register one pi model per Cursor `context` value for each Cursor base model and SDK alias when the model exposes a `context` parameter.
140
142
  - Do not encode `reasoning`, `effort`, `thinking`, or `fast` into pi model IDs.
141
143
  - Prefer stable, readable `@<context>` suffixes that do not conflict with pi's final `:<thinking>` suffix parser.
142
144
  - Sort Cursor models by base ID, then context value in Cursor SDK order before calling `pi.registerProvider()`. Registration order matters for `/model` display and model cycling; `--list-models` sorts output separately.
@@ -177,7 +179,7 @@ Reason:
177
179
 
178
180
  Each registered model must set:
179
181
 
180
- - `id`: context-qualified pi model ID when needed.
182
+ - `id`: context-qualified pi model ID when needed. For SDK aliases, this uses the alias as the pi-visible ID and the alias is sent back to Cursor as `ModelSelection.id`.
181
183
  - `name`: human-readable Cursor display name plus context when useful.
182
184
  - `reasoning`: `true` only if a Cursor `reasoning`, `effort`, or `thinking` parameter can map to pi thinking. This controls pi's thinking UI and `pi --list-models` `thinking` column; it must not be used to claim whether the Cursor model can think internally. Cursor SDK models are thinking-capable even when this is `false`.
183
185
  - `thinkingLevelMap`: model-specific pi-to-Cursor mapping for pi UI, clamping, persistence, and footer display.
@@ -186,7 +188,7 @@ Each registered model must set:
186
188
  - `input`: supported input types. The installed Cursor SDK accepts `SDKUserMessage.images`, and Cursor models are expected to support image input, so advertise `["text", "image"]`.
187
189
  - `cost`: zeroed unless reliable Cursor costs are available.
188
190
 
189
- The extension stores runtime metadata in an internal map keyed by registered pi model ID. That map records the Cursor base model ID, selected context param, default params, and discovered capabilities. `ProviderModelConfig` has no dedicated metadata field, so do not rely on hidden custom fields for this state.
191
+ The extension stores runtime metadata in an internal map keyed by registered pi model ID. That map records the Cursor base catalog model ID, the Cursor selection model ID (base ID or alias), selected context param, default params, and discovered capabilities. `ProviderModelConfig` has no dedicated metadata field, so do not rely on hidden custom fields for this state.
190
192
 
191
193
  ## Dynamic Capabilities
192
194
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",
@@ -38,7 +38,7 @@
38
38
  "test:watch": "vitest"
39
39
  },
40
40
  "dependencies": {
41
- "@cursor/sdk": "^1.0.12"
41
+ "@cursor/sdk": "^1.0.13"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@earendil-works/pi-ai": "*",
@@ -48,7 +48,7 @@
48
48
  "@earendil-works/pi-ai": "^0.74.0",
49
49
  "@earendil-works/pi-coding-agent": "^0.74.0",
50
50
  "typescript": "^6.0.3",
51
- "vitest": "^4.1.5"
51
+ "vitest": "^4.1.6"
52
52
  },
53
53
  "pi": {
54
54
  "extensions": [
@@ -40,6 +40,10 @@ export function loadContextWindowCache(): Map<string, number> {
40
40
  return cache;
41
41
  }
42
42
 
43
+ export function getCachedContextWindowExact(modelId: string): number | undefined {
44
+ return loadContextWindowCache().get(modelId);
45
+ }
46
+
43
47
  export function getCachedContextWindow(modelId: string): number | undefined {
44
48
  const cache = loadContextWindowCache();
45
49
  return cache.get(modelId) ?? cache.get("default");
@@ -12,6 +12,7 @@ import type { CursorPiToolDisplay } from "./cursor-tool-transcript.js";
12
12
  const NATIVE_CURSOR_TOOL_NAMES = ["read", "bash", "ls"] as const;
13
13
  type NativeCursorToolName = (typeof NATIVE_CURSOR_TOOL_NAMES)[number];
14
14
  const NATIVE_CURSOR_TOOL_DISPLAY_ENV = "PI_CURSOR_NATIVE_TOOL_DISPLAY";
15
+ // Registration-only kill switch for users who want transcript fallback without shadowing read/bash/ls.
15
16
  const NATIVE_CURSOR_TOOL_REGISTRATION_ENV = "PI_CURSOR_REGISTER_NATIVE_TOOLS";
16
17
 
17
18
  export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
@@ -19,8 +20,7 @@ export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
19
20
  terminate?: boolean;
20
21
  }
21
22
 
22
- let nativeToolDisplayEnabled = false;
23
- const registeredNativeToolNames = new Set<string>();
23
+ const registeredNativeToolNames = new Set<NativeCursorToolName>();
24
24
  const nativeToolResults = new Map<string, CursorNativeToolDisplayItem>();
25
25
 
26
26
  function readBooleanEnv(name: string): boolean | undefined {
@@ -36,13 +36,16 @@ function isCursorNativeToolDisplayRequested(): boolean {
36
36
  return process.stdout.isTTY === true;
37
37
  }
38
38
 
39
+ function isNativeCursorToolName(toolName: string): toolName is NativeCursorToolName {
40
+ return NATIVE_CURSOR_TOOL_NAMES.some((nativeToolName) => nativeToolName === toolName);
41
+ }
42
+
39
43
  function isCursorNativeToolRegistrationRequested(): boolean {
40
- if (readBooleanEnv(NATIVE_CURSOR_TOOL_REGISTRATION_ENV) === false) return false;
41
- return isCursorNativeToolDisplayRequested();
44
+ return readBooleanEnv(NATIVE_CURSOR_TOOL_REGISTRATION_ENV) !== false && isCursorNativeToolDisplayRequested();
42
45
  }
43
46
 
44
47
  export function isCursorNativeToolDisplayEnabled(): boolean {
45
- return nativeToolDisplayEnabled;
48
+ return registeredNativeToolNames.size > 0;
46
49
  }
47
50
 
48
51
  export function isCursorNativeToolDisplayRuntimeEnabled(): boolean {
@@ -50,7 +53,7 @@ export function isCursorNativeToolDisplayRuntimeEnabled(): boolean {
50
53
  }
51
54
 
52
55
  export function canRenderCursorToolNatively(toolName: string): boolean {
53
- return registeredNativeToolNames.has(toolName);
56
+ return isNativeCursorToolName(toolName) && registeredNativeToolNames.has(toolName);
54
57
  }
55
58
 
56
59
  export function recordCursorNativeToolDisplay(item: CursorNativeToolDisplayItem): void {
@@ -66,7 +69,6 @@ function consumeCursorNativeToolDisplay(id: string): CursorNativeToolDisplayItem
66
69
 
67
70
  export const __testUtils = {
68
71
  reset(): void {
69
- nativeToolDisplayEnabled = false;
70
72
  registeredNativeToolNames.clear();
71
73
  nativeToolResults.clear();
72
74
  },
@@ -103,15 +105,13 @@ function registerNativeCursorTool(pi: ExtensionAPI, toolName: NativeCursorToolNa
103
105
  pi.registerTool(wrapNativeCursorTool(createLsToolDefinition(cwd)));
104
106
  }
105
107
 
106
- function getExistingToolOwner(pi: ExtensionAPI, toolName: NativeCursorToolName): string | undefined {
108
+ function hasNonBuiltinTool(pi: ExtensionAPI, toolName: NativeCursorToolName): boolean {
107
109
  const existingTool = pi.getAllTools().find((tool) => tool.name === toolName);
108
- if (!existingTool || existingTool.sourceInfo.source === "builtin") return undefined;
109
- return existingTool.sourceInfo.path ?? existingTool.sourceInfo.source;
110
+ return existingTool !== undefined && existingTool.sourceInfo.source !== "builtin";
110
111
  }
111
112
 
112
113
  function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: ExtensionContext): void {
113
114
  if (!isCursorNativeToolRegistrationRequested()) {
114
- nativeToolDisplayEnabled = false;
115
115
  registeredNativeToolNames.clear();
116
116
  return;
117
117
  }
@@ -120,8 +120,7 @@ function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: ExtensionCont
120
120
  const skippedToolNames: string[] = [];
121
121
  for (const toolName of NATIVE_CURSOR_TOOL_NAMES) {
122
122
  if (registeredNativeToolNames.has(toolName)) continue;
123
- const existingOwner = getExistingToolOwner(pi, toolName);
124
- if (existingOwner) {
123
+ if (hasNonBuiltinTool(pi, toolName)) {
125
124
  skippedToolNames.push(toolName);
126
125
  continue;
127
126
  }
@@ -129,7 +128,6 @@ function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: ExtensionCont
129
128
  registeredNativeToolNames.add(toolName);
130
129
  }
131
130
 
132
- nativeToolDisplayEnabled = registeredNativeToolNames.size > 0;
133
131
  if (skippedToolNames.length > 0 && readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) === true && ctx.hasUI) {
134
132
  ctx.ui.notify(
135
133
  `Cursor native tool replay skipped for ${skippedToolNames.join(", ")} because another extension already provides ${skippedToolNames.length === 1 ? "that tool" : "those tools"}. Cursor will use scrubbed activity transcripts for skipped tools.`,
@@ -139,7 +137,7 @@ function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: ExtensionCont
139
137
  }
140
138
 
141
139
  export function registerCursorNativeToolDisplay(pi: ExtensionAPI): void {
142
- pi.on("session_start", async (_event, ctx) => {
140
+ pi.on("session_start", (_event, ctx) => {
143
141
  registerAvailableNativeCursorTools(pi, ctx);
144
142
  });
145
143
  }
@@ -70,6 +70,7 @@ interface CursorNativeLiveRun {
70
70
  id: string;
71
71
  agent: SDKAgent;
72
72
  promptInputTokens: number;
73
+ promptInputTokensReported: boolean;
73
74
  pendingEvents: CursorNativeQueuedEvent[];
74
75
  textDeltas: string[];
75
76
  finalText?: string;
@@ -374,6 +375,13 @@ function collectCursorNativeToolBatch(run: CursorNativeLiveRun): CursorNativeToo
374
375
  return tools;
375
376
  }
376
377
 
378
+ function takeCursorNativePromptInputTokens(run: CursorNativeLiveRun): number {
379
+ // Native replay can split one Cursor run into multiple pi turns; count prompt input once.
380
+ if (run.promptInputTokensReported) return 0;
381
+ run.promptInputTokensReported = true;
382
+ return run.promptInputTokens;
383
+ }
384
+
377
385
  function emitCursorNativeToolUseTurn(
378
386
  stream: AssistantMessageEventStream,
379
387
  partial: AssistantMessage,
@@ -396,7 +404,7 @@ function emitCursorNativeToolUseTurn(
396
404
  if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
397
405
  recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate });
398
406
  }
399
- setApproximateUsage(partial, run.promptInputTokens, outputText);
407
+ setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
400
408
  partial.stopReason = "toolUse";
401
409
  stream.push({ type: "done", reason: "toolUse", message: partial });
402
410
  }
@@ -455,7 +463,7 @@ async function emitCursorNativeRunNextTurn(
455
463
  if (!outputText) {
456
464
  outputText += await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(run.finalText ?? run.textDeltas.join("")));
457
465
  }
458
- setApproximateUsage(partial, run.promptInputTokens, outputText);
466
+ setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
459
467
  partial.stopReason = "stop";
460
468
  stream.push({ type: "done", reason: "stop", message: partial });
461
469
  await disposeCursorNativeRun(run);
@@ -534,12 +542,13 @@ export function streamCursor(
534
542
  const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
535
543
  const nativeReplayId = createCursorNativeReplayId();
536
544
  const textDeltas: string[] = [];
537
- let liveStreamClosed = false;
545
+ let nativeToolReplayStarted = false;
538
546
  const liveRun: CursorNativeLiveRun | undefined = useNativeToolReplay
539
547
  ? {
540
548
  id: nativeReplayId,
541
549
  agent,
542
550
  promptInputTokens,
551
+ promptInputTokensReported: false,
543
552
  pendingEvents: [],
544
553
  textDeltas,
545
554
  done: false,
@@ -651,6 +660,7 @@ export function streamCursor(
651
660
  completedToolFingerprints.add(fingerprint);
652
661
 
653
662
  if (useNativeToolReplay && canRenderCursorToolNatively(display.toolName) && liveRun) {
663
+ nativeToolReplayStarted = true;
654
664
  const id = `${nativeReplayId}-tool-${++nativeToolDisplayCounter}`;
655
665
  queueCursorNativeEvent(liveRun, {
656
666
  type: "tool",
@@ -672,19 +682,19 @@ export function streamCursor(
672
682
 
673
683
  if (update.type === "text-delta") {
674
684
  textDeltas.push(update.text);
675
- if (liveRun && liveStreamClosed) {
685
+ if (liveRun && nativeToolReplayStarted) {
676
686
  queueCursorNativeEvent(liveRun, { type: "text-delta", text: update.text });
677
687
  } else if (!useNativeToolReplay) {
678
688
  appendLiveTextDelta(update.text);
679
689
  }
680
690
  } else if (update.type === "thinking-delta") {
681
- if (liveRun && liveStreamClosed) {
691
+ if (liveRun && nativeToolReplayStarted) {
682
692
  queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: update.text });
683
693
  } else {
684
694
  appendTraceDelta(update.text);
685
695
  }
686
696
  } else if (update.type === "thinking-completed") {
687
- if (liveRun && liveStreamClosed) {
697
+ if (liveRun && nativeToolReplayStarted) {
688
698
  queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
689
699
  } else {
690
700
  closeTraceBlock();
@@ -697,7 +707,7 @@ export function streamCursor(
697
707
  handleCompletedToolCall(mergedToolCall);
698
708
  } else if (update.type === "summary") {
699
709
  const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
700
- if (liveRun && liveStreamClosed) {
710
+ if (liveRun && nativeToolReplayStarted) {
701
711
  queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: summary });
702
712
  } else {
703
713
  appendTraceDelta(summary);
@@ -754,7 +764,6 @@ export function streamCursor(
754
764
  await waitForCursorNativeRunProgress(liveRun, options?.signal);
755
765
  await settleCursorNativeToolBatch(liveRun);
756
766
  closeTraceBlock();
757
- liveStreamClosed = true;
758
767
  await emitCursorNativeRunNextTurn(stream, partial, liveRun, options?.signal);
759
768
  agent = null;
760
769
  return;
@@ -7,7 +7,7 @@ import type {
7
7
  } from "@cursor/sdk";
8
8
  import { AuthStorage, type ProviderModelConfig } from "@earendil-works/pi-coding-agent";
9
9
  import type { ModelThinkingLevel, ThinkingLevelMap } from "@earendil-works/pi-ai";
10
- import { getCachedContextWindow } from "./context-window-cache.js";
10
+ import { getCachedContextWindow, getCachedContextWindowExact } from "./context-window-cache.js";
11
11
 
12
12
  const CURSOR_PROVIDER_ID = "cursor";
13
13
  const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
@@ -217,6 +217,7 @@ async function getDiscoveryApiKey(): Promise<string | undefined> {
217
217
  export interface CursorModelMetadata {
218
218
  piModelId: string;
219
219
  baseModelId: string;
220
+ selectionModelId: string;
220
221
  displayName: string;
221
222
  defaultParams: ModelParameterValue[];
222
223
  context?: string;
@@ -340,23 +341,25 @@ function getParamValue(params: ModelParameterValue[], id: string): string | unde
340
341
  return params.find((param) => param.id === id)?.value;
341
342
  }
342
343
 
343
- function encodePiModelId(baseModelId: string, context?: string): string {
344
- return context ? `${baseModelId}@${context}` : baseModelId;
344
+ function encodePiModelId(modelId: string, context?: string): string {
345
+ return context ? `${modelId}@${context}` : modelId;
345
346
  }
346
347
 
347
- function getModelName(item: ModelListItem, context?: string): string {
348
+ function getModelName(item: ModelListItem, context?: string, alias?: string): string {
348
349
  const displayName = item.displayName || item.id;
349
- return context ? `${displayName} @ ${context}` : displayName;
350
+ const baseName = alias ? `${displayName} (${alias})` : displayName;
351
+ return context ? `${baseName} @ ${context}` : baseName;
350
352
  }
351
353
 
352
- function getContextWindow(piModelId: string, context?: string): number {
354
+ function getContextWindow(piModelId: string, context?: string, baseModelId?: string): number {
353
355
  if (context) return parseContextWindow(context) ?? FALLBACK_CONTEXT_WINDOW;
354
- return getCachedContextWindow(piModelId) ?? FALLBACK_CONTEXT_WINDOW;
356
+ return getCachedContextWindowExact(piModelId) ?? (baseModelId ? getCachedContextWindow(baseModelId) : undefined) ?? FALLBACK_CONTEXT_WINDOW;
355
357
  }
356
358
 
357
359
  function toMetadata(
358
360
  item: ModelListItem,
359
361
  piModelId: string,
362
+ selectionModelId: string,
360
363
  defaultParams: ModelParameterValue[],
361
364
  context: string | undefined,
362
365
  ): CursorModelMetadata {
@@ -365,10 +368,11 @@ function toMetadata(
365
368
  return {
366
369
  piModelId,
367
370
  baseModelId: item.id,
371
+ selectionModelId,
368
372
  displayName: item.displayName || item.id,
369
373
  defaultParams: cloneParams(defaultParams),
370
374
  ...(context ? { context } : {}),
371
- contextWindow: getContextWindow(piModelId, context),
375
+ contextWindow: getContextWindow(piModelId, context, item.id),
372
376
  supportsFast: getParameter(item, "fast") !== undefined,
373
377
  defaultFast: fastValue === "true",
374
378
  supportsReasoning: thinkingLevelMap !== undefined,
@@ -400,18 +404,40 @@ function getContextValues(item: ModelListItem): string[] {
400
404
  return getParameter(item, "context")?.values.map((value) => value.value) ?? [];
401
405
  }
402
406
 
403
- function toModelConfigs(item: ModelListItem): ProviderModelConfig[] {
407
+ function getModelIds(item: ModelListItem, reservedBaseModelIds: Set<string>): string[] {
408
+ const ids = [item.id];
409
+ for (const rawAlias of item.aliases ?? []) {
410
+ const alias = rawAlias.trim();
411
+ if (!alias || alias === item.id || ids.includes(alias) || reservedBaseModelIds.has(alias)) continue;
412
+ ids.push(alias);
413
+ }
414
+ return ids;
415
+ }
416
+
417
+ function toModelConfigs(
418
+ item: ModelListItem,
419
+ usedPiModelIds: Set<string>,
420
+ reservedBaseModelIds: Set<string>,
421
+ ): ProviderModelConfig[] {
404
422
  const defaultParams = getDefaultParams(item);
405
423
  const contextValues = getContextValues(item);
406
424
  const contexts = contextValues.length > 0 ? contextValues : [undefined];
425
+ const configs: ProviderModelConfig[] = [];
426
+
427
+ for (const selectionModelId of getModelIds(item, reservedBaseModelIds)) {
428
+ const alias = selectionModelId === item.id ? undefined : selectionModelId;
429
+ for (const context of contexts) {
430
+ const params = context ? replaceParam(defaultParams, "context", context) : defaultParams;
431
+ const piModelId = encodePiModelId(selectionModelId, context);
432
+ if (usedPiModelIds.has(piModelId)) continue;
433
+ usedPiModelIds.add(piModelId);
434
+ const metadata = toMetadata(item, piModelId, selectionModelId, params, context);
435
+ metadataByPiModelId.set(piModelId, metadata);
436
+ configs.push(toModelConfig(metadata, getModelName(item, context, alias)));
437
+ }
438
+ }
407
439
 
408
- return contexts.map((context) => {
409
- const params = context ? replaceParam(defaultParams, "context", context) : defaultParams;
410
- const piModelId = encodePiModelId(item.id, context);
411
- const metadata = toMetadata(item, piModelId, params, context);
412
- metadataByPiModelId.set(piModelId, metadata);
413
- return toModelConfig(metadata, getModelName(item, context));
414
- });
440
+ return configs;
415
441
  }
416
442
 
417
443
  function sortModelsByBaseId(items: ModelListItem[]): ModelListItem[] {
@@ -420,7 +446,9 @@ function sortModelsByBaseId(items: ModelListItem[]): ModelListItem[] {
420
446
 
421
447
  function registerModelItems(items: ModelListItem[]): ProviderModelConfig[] {
422
448
  metadataByPiModelId.clear();
423
- return sortModelsByBaseId(items).flatMap(toModelConfigs);
449
+ const usedPiModelIds = new Set<string>();
450
+ const reservedBaseModelIds = new Set(items.map((item) => item.id));
451
+ return sortModelsByBaseId(items).flatMap((item) => toModelConfigs(item, usedPiModelIds, reservedBaseModelIds));
424
452
  }
425
453
 
426
454
  export function getCursorModelMetadata(modelId: string): CursorModelMetadata | undefined {
@@ -501,7 +529,7 @@ export function buildCursorModelSelection(
501
529
  setParam(params, "fast", fastEnabled ? "true" : "false");
502
530
  }
503
531
 
504
- return params.length > 0 ? { id: metadata.baseModelId, params } : { id: metadata.baseModelId };
532
+ return params.length > 0 ? { id: metadata.selectionModelId, params } : { id: metadata.selectionModelId };
505
533
  }
506
534
 
507
535
  function useFallbackModels(options: DiscoverModelsOptions, issue: CursorModelFallbackIssue): ProviderModelConfig[] {