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
@@ -13,7 +13,7 @@ import {
13
13
  type IncompleteCursorToolRunOutcome,
14
14
  } from "./cursor-incomplete-tool-visibility.js";
15
15
  import { getToolName } from "./cursor-transcript-utils.js";
16
- import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
16
+ import { getNormalizedCursorToolName } from "./cursor-tool-visibility.js";
17
17
  import { buildCursorPiToolDisplay } from "./cursor-tool-transcript.js";
18
18
  import { getField } from "./cursor-record-utils.js";
19
19
  import { CursorTurnDisplayRouter } from "./cursor-provider-turn-display-router.js";
@@ -24,6 +24,7 @@ import {
24
24
  import { resolveCursorToolCompletion } from "./cursor-provider-turn-sdk-normalizer.js";
25
25
  import {
26
26
  CursorShellOutputTracker,
27
+ formatCursorShellOutputProgressText,
27
28
  getCursorShellOutputDelta,
28
29
  isCursorShellToolCall,
29
30
  } from "./cursor-provider-turn-shell-output.js";
@@ -32,10 +33,6 @@ import {
32
33
  getToolFingerprint,
33
34
  } from "./cursor-provider-turn-tool-ledger.js";
34
35
 
35
- function getNormalizedCursorToolName(toolCall: unknown): string {
36
- return classifyCursorToolVisibility(toolCall).normalizedName;
37
- }
38
-
39
36
  export interface CursorSdkTurnCoordinatorOptions {
40
37
  stream: AssistantMessageEventStream;
41
38
  partial: AssistantMessage;
@@ -207,11 +204,24 @@ export class CursorSdkTurnCoordinator {
207
204
  identity: resolution.identity,
208
205
  source: resolution.source,
209
206
  });
207
+ if (resolution.matchedStartedCallId && resolution.matchedStartedCallId !== update.callId) {
208
+ this.ledger.recordCompletedIdentity(`cursor-tool:${resolution.matchedStartedCallId}`);
209
+ }
210
210
  return;
211
211
  }
212
212
  if (update.type === "shell-output-delta") {
213
213
  const delta = getCursorShellOutputDelta(update);
214
- if (delta) this.shellOutput.appendShellOutputDelta(delta);
214
+ if (delta) {
215
+ const progress = this.shellOutput.appendShellOutputDelta(delta);
216
+ const progressText = progress ? formatCursorShellOutputProgressText(progress, this.resolvedApiKey) : undefined;
217
+ if (progressText) {
218
+ if (this.liveRun) {
219
+ cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: progressText });
220
+ } else {
221
+ this.contentEmitter.appendThinkingDelta(progressText);
222
+ }
223
+ }
224
+ }
215
225
  return;
216
226
  }
217
227
  if (update.type === "summary") {
@@ -10,7 +10,11 @@ import {
10
10
  type IncompleteCursorToolDiscardReason,
11
11
  } from "./cursor-incomplete-tool-visibility.js";
12
12
  import { scrubPiToolDisplay, scrubSensitiveText } from "./cursor-sensitive-text.js";
13
- import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText } from "./cursor-tool-transcript.js";
13
+ import {
14
+ buildCursorPiToolDisplay,
15
+ formatCursorToolTranscript,
16
+ getCursorCreatePlanText,
17
+ } from "./cursor-tool-transcript.js";
14
18
  import { getToolName } from "./cursor-transcript-utils.js";
15
19
  import type { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
16
20
  import type { CursorToolDisplaySource } from "./cursor-provider-turn-tool-ledger.js";
@@ -1,10 +1,10 @@
1
1
  import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
2
2
  import {
3
+ cursorLiveRuns,
3
4
  drainCursorLiveRunTurn,
4
5
  flushPendingCursorLiveRunTraceEventsToStream,
5
6
  settleCursorLiveToolBatch,
6
7
  } from "./cursor-provider-live-run-drain.js";
7
- import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
8
8
  import {
9
9
  buildIncompleteCursorToolRunOutcome,
10
10
  type IncompleteCursorToolRunOutcomeInput,
@@ -8,13 +8,9 @@ import {
8
8
  formatCursorToolLifecycleProgressText,
9
9
  isCursorToolLifecycleEligible,
10
10
  } from "./cursor-tool-lifecycle.js";
11
- import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
11
+ import { getNormalizedCursorToolName } from "./cursor-tool-visibility.js";
12
12
  import { getStartedToolCallFingerprint } from "./cursor-provider-turn-tool-ledger.js";
13
13
 
14
- function getNormalizedCursorToolName(toolCall: unknown): string {
15
- return classifyCursorToolVisibility(toolCall).normalizedName;
16
- }
17
-
18
14
  export interface CursorToolLifecycleEmitterOptions {
19
15
  liveRun?: CursorLiveRun;
20
16
  resolvedApiKey?: string;
@@ -8,23 +8,27 @@ import {
8
8
  resetSessionCursorAgent,
9
9
  } from "./cursor-session-agent.js";
10
10
  import type { CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge.js";
11
- import { estimateCursorPromptInputTokens, getCursorPromptOptions } from "./cursor-usage-accounting.js";
11
+ import { estimateCursorPromptTokens } from "./context.js";
12
+ import { getCursorPromptOptions } from "./cursor-usage-accounting.js";
12
13
  import { getActiveContextToolNames } from "./cursor-context-tools.js";
13
14
  import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
14
- import { abandonSessionCursorAgent, cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
15
- import { createCursorNativeReplayId } from "./cursor-provider-live-run-drain.js";
16
- import { getEffectiveCursorAgentMode, getEffectiveFastForModelId } from "./cursor-state.js";
15
+ import {
16
+ abandonSessionCursorAgent,
17
+ createCursorNativeReplayId,
18
+ cursorLiveRuns,
19
+ } from "./cursor-provider-live-run-drain.js";
20
+ import { getCursorProviderAgentModeOrThrow, getEffectiveFastForModelId } from "./cursor-state.js";
17
21
  import { buildCursorModelSelection } from "./model-discovery.js";
18
22
  import { getEffectiveCursorSettingSources } from "./cursor-setting-sources.js";
19
- import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-snapshot.js";
23
+ import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
20
24
  import {
21
25
  buildCursorToolManifestText,
22
26
  resolveCursorToolManifestEnabled,
23
27
  } from "./cursor-tool-manifest.js";
24
- import { isCursorNativeToolDisplayRuntimeEnabled } from "./cursor-native-tool-display.js";
28
+ import { isCursorNativeToolDisplayRuntimeEnabled } from "./cursor-native-tool-display-state.js";
25
29
  import { MISSING_CURSOR_API_KEY_MESSAGE } from "./cursor-provider-errors.js";
26
30
  import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
27
- import { resolveCursorApiKey } from "./cursor-provider-turn-api-key.js";
31
+ import { resolveCursorApiKey } from "./cursor-api-key.js";
28
32
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
29
33
  import type {
30
34
  CursorProviderTurnPrepareResult,
@@ -53,7 +57,7 @@ export async function prepareCursorProviderTurn(
53
57
 
54
58
  try {
55
59
  const fastEnabled = getEffectiveFastForModelId(model.id);
56
- const agentMode = getEffectiveCursorAgentMode();
60
+ const agentMode = getCursorProviderAgentModeOrThrow();
57
61
  const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
58
62
  const settingSources = getEffectiveCursorSettingSources();
59
63
  const { Agent } = await loadCursorSdk();
@@ -116,7 +120,7 @@ export async function prepareCursorProviderTurn(
116
120
  images: prompt.images.length > 0 ? prompt.images : undefined,
117
121
  };
118
122
  const sessionBridgeRun = bridgeRun;
119
- const promptInputTokens = estimateCursorPromptInputTokens(prompt, promptOptions);
123
+ const promptInputTokens = estimateCursorPromptTokens(prompt, promptOptions);
120
124
  const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
121
125
  const activeToolNames = getActiveContextToolNames(context);
122
126
  sdkEventDebug?.recordProviderMeta({
@@ -1,6 +1,6 @@
1
1
  import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
2
2
  import { drainExistingCursorLiveRunBeforeSend } from "./cursor-provider-live-run-drain.js";
3
- import { getCursorSessionCwd } from "./cursor-session-cwd.js";
3
+ import { getCursorSessionCwd } from "./cursor-session-scope.js";
4
4
  import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
5
5
  import { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
6
6
  import { awaitFinalizeCursorRunOutcome } from "./cursor-provider-turn-finalize.js";
@@ -17,7 +17,6 @@ import type {
17
17
  CursorProviderTurnSendResult,
18
18
  } from "./cursor-provider-turn-types.js";
19
19
 
20
- export { resolveCursorApiKey } from "./cursor-provider-turn-api-key.js";
21
20
  export type { CursorProviderTurnRunnerParams } from "./cursor-provider-turn-types.js";
22
21
 
23
22
  export class CursorProviderTurnRunner {
@@ -34,13 +33,6 @@ export class CursorProviderTurnRunner {
34
33
  if (this.options?.signal?.aborted) throw new CursorLiveRunAbortError();
35
34
  }
36
35
 
37
- private discardIncompleteTools(
38
- prepared: CursorProviderTurnPrepareResult | undefined,
39
- outcome: import("./cursor-incomplete-tool-visibility.js").IncompleteCursorToolRunOutcomeInput,
40
- ): void {
41
- discardIncompleteToolsFromPrepared(prepared, outcome);
42
- }
43
-
44
36
  async run(sdkProcessErrorGuard: ReturnType<typeof installCursorSdkProcessErrorGuard>): Promise<void> {
45
37
  const { stream, partial, model, context, options, sdkEventDebugRef } = this.params;
46
38
  let prepared: CursorProviderTurnPrepareResult | undefined;
@@ -94,13 +86,13 @@ export class CursorProviderTurnRunner {
94
86
  send,
95
87
  prepared,
96
88
  modelId: model.id,
97
- discardIncompleteTools: (outcome) => this.discardIncompleteTools(prepared, outcome),
89
+ discardIncompleteTools: (outcome) => discardIncompleteToolsFromPrepared(prepared, outcome),
98
90
  });
99
91
  await emitCursorLiveTurn({
100
92
  params: this.params,
101
93
  prepared,
102
94
  sdkEventDebug: this.sdkEventDebug,
103
- discardIncompleteTools: (outcome) => this.discardIncompleteTools(prepared, outcome),
95
+ discardIncompleteTools: (outcome) => discardIncompleteToolsFromPrepared(prepared, outcome),
104
96
  });
105
97
  return;
106
98
  }
@@ -1,4 +1,4 @@
1
- import { mergeCursorToolCalls } from "./cursor-tool-transcript.js";
1
+ import { asRecord } from "./cursor-record-utils.js";
2
2
  import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
3
3
  import {
4
4
  mergeShellOutputDeltasIntoCursorToolCall,
@@ -8,6 +8,22 @@ import type { CursorToolCompletionLedger } from "./cursor-provider-turn-tool-led
8
8
 
9
9
  export type CursorToolCompletionSource = "delta" | "step";
10
10
 
11
+ function mergeCursorToolCalls(startedToolCall: unknown, completedToolCall: unknown): unknown {
12
+ const started = asRecord(startedToolCall);
13
+ const completed = asRecord(completedToolCall);
14
+ if (!started) return completedToolCall;
15
+ if (!completed) return startedToolCall;
16
+ return {
17
+ ...started,
18
+ ...completed,
19
+ name: completed.name ?? started.name,
20
+ type: completed.type ?? started.type,
21
+ args: completed.args ?? started.args,
22
+ input: completed.input ?? started.input,
23
+ result: completed.result ?? started.result,
24
+ };
25
+ }
26
+
11
27
  export type ToolCompletionResolution =
12
28
  | { action: "ignore-bridge"; identity?: string }
13
29
  | {
@@ -47,15 +63,22 @@ export function resolveCursorToolCompletion(options: ResolveCursorToolCompletion
47
63
  const callId = options.callId;
48
64
  identity = typeof callId === "string" ? `cursor-tool:${callId}` : undefined;
49
65
  resolvedToolCall = mergeCursorToolCalls(options.startedToolCall, options.toolCall);
50
- if (typeof callId === "string") {
66
+ if (typeof callId === "string" && options.ledger.hasStartedToolCall(callId)) {
51
67
  options.onClearStartedCallId?.(callId);
52
68
  options.ledger.clearStartedToolCall(callId);
69
+ } else {
70
+ matchedStartedCallId = options.ledger.removeStartedToolCallForStep(options.toolCall, callId);
71
+ if (matchedStartedCallId) options.onClearStartedCallId?.(matchedStartedCallId);
53
72
  }
54
73
  resolvedToolCall = mergeShellOutputDeltasIntoCursorToolCall(
55
74
  resolvedToolCall,
56
- typeof callId === "string" ? options.shellOutput.takeDeltasForCall(callId) : undefined,
75
+ matchedStartedCallId
76
+ ? options.shellOutput.takeDeltasForCall(matchedStartedCallId)
77
+ : typeof callId === "string"
78
+ ? options.shellOutput.takeDeltasForCall(callId)
79
+ : undefined,
57
80
  );
58
- source = identity ? "started" : "fallback";
81
+ source = identity || matchedStartedCallId ? "started" : "fallback";
59
82
  } else {
60
83
  matchedStartedCallId = options.ledger.removeStartedToolCallForStep(options.toolCall, options.callId);
61
84
  if (matchedStartedCallId) {
@@ -77,7 +100,7 @@ export function resolveCursorToolCompletion(options: ResolveCursorToolCompletion
77
100
  }
78
101
 
79
102
  if (options.source === "delta") {
80
- return { action: "handle", toolCall: resolvedToolCall, identity, source };
103
+ return { action: "handle", toolCall: resolvedToolCall, identity, source, matchedStartedCallId };
81
104
  }
82
105
  return {
83
106
  action: "handle",
@@ -1,7 +1,7 @@
1
1
  import type { SendOptions } from "@cursor/sdk";
2
+ import { countCursorAgentMessages } from "./cursor-agent-message-web-tools.js";
2
3
  import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
3
4
  import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
4
- import { getCursorAgentMessageOffset } from "./cursor-provider-turn-message-offset.js";
5
5
  import type { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
6
6
  import type {
7
7
  CursorProviderTurnRunnerParams,
@@ -41,7 +41,12 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
41
41
  try {
42
42
  abortRegistration?.signal.addEventListener("abort", abortListener, { once: true });
43
43
  throwIfAborted();
44
- const cursorAgentMessageOffset = await getCursorAgentMessageOffset(agent.agentId, cwd, sdkEventDebug);
44
+ let cursorAgentMessageOffset: number | undefined;
45
+ try {
46
+ cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd);
47
+ } catch (error) {
48
+ sdkEventDebug?.recordError("cursor_agent_message_count", error);
49
+ }
45
50
  throwIfAborted();
46
51
  sdkEventDebug?.recordSendMeta({
47
52
  mode: meta.sendPlan.mode,
@@ -1,5 +1,7 @@
1
1
  import type { InteractionUpdate } from "@cursor/sdk";
2
2
  import { asRecord, getField, hasUsableText } from "./cursor-record-utils.js";
3
+ import { scrubSensitiveText } from "./cursor-sensitive-text.js";
4
+ import { truncateCursorDisplayLine } from "./cursor-display-text.js";
3
5
  import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
4
6
 
5
7
  export interface CursorShellOutputDelta {
@@ -12,6 +14,12 @@ export interface CursorShellOutputDeltas {
12
14
  stderr: string[];
13
15
  }
14
16
 
17
+ export interface CursorShellOutputProgressDelta extends CursorShellOutputDelta {
18
+ callId: string;
19
+ }
20
+
21
+ const SHELL_OUTPUT_PROGRESS_MAX_DELTAS_PER_CALL = 3;
22
+
15
23
  export function isCursorShellToolCall(toolCall: unknown): boolean {
16
24
  return classifyCursorToolVisibility(toolCall).normalizedKey === "shell";
17
25
  }
@@ -27,6 +35,22 @@ export function getCursorShellOutputDelta(update: InteractionUpdate): CursorShel
27
35
  return { stream: eventCase, data };
28
36
  }
29
37
 
38
+ function getCursorShellOutputProgressPreview(data: string): string | undefined {
39
+ return data
40
+ .split(/\r?\n/)
41
+ .map((line) => line.trim())
42
+ .find((line) => line.length > 0);
43
+ }
44
+
45
+ export function formatCursorShellOutputProgressText(
46
+ progress: CursorShellOutputProgressDelta,
47
+ apiKey?: string,
48
+ ): string | undefined {
49
+ const preview = getCursorShellOutputProgressPreview(progress.data);
50
+ if (!preview) return undefined;
51
+ return `Cursor shell ${progress.stream}: ${truncateCursorDisplayLine(scrubSensitiveText(preview, apiKey), 160)}\n`;
52
+ }
53
+
30
54
  export function mergeShellOutputDeltasIntoCursorToolCall(
31
55
  toolCall: unknown,
32
56
  deltas: CursorShellOutputDeltas | undefined,
@@ -65,6 +89,7 @@ export class CursorShellOutputTracker {
65
89
  private readonly activeShellCallIds = new Set<string>();
66
90
  private readonly ambiguousShellOutputCallIds = new Set<string>();
67
91
  private readonly shellOutputDeltasByCallId = new Map<string, CursorShellOutputDeltas>();
92
+ private readonly shellOutputProgressCountsByCallId = new Map<string, number>();
68
93
 
69
94
  onShellToolStarted(callId: string): void {
70
95
  this.activeShellCallIds.add(callId);
@@ -73,29 +98,38 @@ export class CursorShellOutputTracker {
73
98
  onShellToolCleared(callId: string): void {
74
99
  this.activeShellCallIds.delete(callId);
75
100
  this.ambiguousShellOutputCallIds.delete(callId);
101
+ this.shellOutputProgressCountsByCallId.delete(callId);
76
102
  }
77
103
 
78
- appendShellOutputDelta(delta: CursorShellOutputDelta): void {
104
+ appendShellOutputDelta(delta: CursorShellOutputDelta): CursorShellOutputProgressDelta | undefined {
79
105
  if (this.activeShellCallIds.size !== 1) {
80
106
  for (const activeCallId of this.activeShellCallIds) {
81
107
  this.ambiguousShellOutputCallIds.add(activeCallId);
82
108
  this.shellOutputDeltasByCallId.delete(activeCallId);
109
+ this.shellOutputProgressCountsByCallId.delete(activeCallId);
83
110
  }
84
- return;
111
+ return undefined;
85
112
  }
86
113
  const [callId] = this.activeShellCallIds;
87
- if (!callId || this.ambiguousShellOutputCallIds.has(callId)) return;
114
+ if (!callId || this.ambiguousShellOutputCallIds.has(callId)) return undefined;
88
115
  let deltas = this.shellOutputDeltasByCallId.get(callId);
89
116
  if (!deltas) {
90
117
  deltas = { stdout: [], stderr: [] };
91
118
  this.shellOutputDeltasByCallId.set(callId, deltas);
92
119
  }
93
120
  deltas[delta.stream].push(delta.data);
121
+
122
+ if (!getCursorShellOutputProgressPreview(delta.data)) return undefined;
123
+ const progressCount = this.shellOutputProgressCountsByCallId.get(callId) ?? 0;
124
+ if (progressCount >= SHELL_OUTPUT_PROGRESS_MAX_DELTAS_PER_CALL) return undefined;
125
+ this.shellOutputProgressCountsByCallId.set(callId, progressCount + 1);
126
+ return { ...delta, callId };
94
127
  }
95
128
 
96
129
  takeDeltasForCall(callId: string): CursorShellOutputDeltas | undefined {
97
130
  const deltas = this.shellOutputDeltasByCallId.get(callId);
98
131
  this.shellOutputDeltasByCallId.delete(callId);
132
+ this.shellOutputProgressCountsByCallId.delete(callId);
99
133
  return deltas;
100
134
  }
101
135
 
@@ -103,5 +137,6 @@ export class CursorShellOutputTracker {
103
137
  this.activeShellCallIds.clear();
104
138
  this.ambiguousShellOutputCallIds.clear();
105
139
  this.shellOutputDeltasByCallId.clear();
140
+ this.shellOutputProgressCountsByCallId.clear();
106
141
  }
107
142
  }
@@ -61,7 +61,7 @@ export type CursorProviderTurnRuntime = DirectCursorProviderTurnRuntime | LiveCu
61
61
  * Send, finalize, and cleanup phases receive this immutable object instead of
62
62
  * keeping parallel liveRun/turnCoordinator/resource bags in sync by convention.
63
63
  */
64
- export interface PreparedCursorProviderTurn {
64
+ export interface CursorProviderTurnPrepareResult {
65
65
  agent: SDKAgent;
66
66
  cwd: string;
67
67
  payload: CursorProviderTurnSendPayload;
@@ -74,8 +74,6 @@ export interface PreparedCursorProviderTurn {
74
74
  runtime: CursorProviderTurnRuntime;
75
75
  }
76
76
 
77
- export type CursorProviderTurnPrepareResult = PreparedCursorProviderTurn;
78
-
79
77
  export interface CursorProviderTurnSend {
80
78
  run: Awaited<ReturnType<SDKAgent["send"]>>;
81
79
  cursorAgentMessageOffset: number | undefined;
@@ -8,6 +8,7 @@ import {
8
8
  type SimpleStreamOptions,
9
9
  } from "@earendil-works/pi-ai";
10
10
  import {
11
+ cursorLiveRuns,
11
12
  DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
12
13
  getPendingCursorLiveRun,
13
14
  hasTrailingUserMessagesAfterToolResults,
@@ -15,12 +16,12 @@ import {
15
16
  resetCursorNativeReplayIdleDisposeMs,
16
17
  setCursorNativeReplayIdleDisposeMs,
17
18
  } from "./cursor-provider-live-run-drain.js";
18
- import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
19
19
  import { disposeAllSessionCursorAgents } from "./cursor-session-agent.js";
20
20
  import { attachCursorSdkEventDebugPiStreamTap, type CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
21
21
  import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
22
22
  import { sanitizeCursorProviderError } from "./cursor-provider-errors.js";
23
- import { CursorProviderTurnRunner, resolveCursorApiKey } from "./cursor-provider-turn-runner.js";
23
+ import { resolveCursorApiKey } from "./cursor-api-key.js";
24
+ import { CursorProviderTurnRunner } from "./cursor-provider-turn-runner.js";
24
25
 
25
26
  function makeInitialMessage(model: Model<Api>): AssistantMessage {
26
27
  return {
@@ -1,8 +1,9 @@
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 { Text } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
4
  import { isCursorModel } from "./cursor-model.js";
5
- import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge.js";
5
+ import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
6
+ import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
6
7
 
7
8
  export const CURSOR_ASK_QUESTION_TOOL_NAME = "cursor_ask_question";
8
9
 
@@ -35,12 +36,7 @@ interface CursorQuestionDetails {
35
36
  cancelled: boolean;
36
37
  }
37
38
 
38
- interface CursorQuestionToolExtensionApi extends Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools"> {
39
- on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
40
- on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent>): void;
41
- on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
42
- on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
43
- }
39
+ interface CursorQuestionToolExtensionApi extends Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools">, CursorModelLifecycleExtensionApi {}
44
40
 
45
41
  type RawQuestionOption = string | { label?: string; value?: string; description?: string };
46
42
 
@@ -232,16 +228,7 @@ export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi):
232
228
  },
233
229
  });
234
230
 
235
- pi.on("session_start", (_event, ctx) => {
236
- syncCursorQuestionToolForModel(pi, ctx.model);
237
- });
238
- pi.on("before_agent_start", (_event, ctx) => {
231
+ registerCursorModelLifecycle(pi, (ctx) => {
239
232
  syncCursorQuestionToolForModel(pi, ctx.model);
240
233
  });
241
- pi.on("turn_start", (_event, ctx) => {
242
- syncCursorQuestionToolForModel(pi, ctx.model);
243
- });
244
- pi.on("model_select", (event) => {
245
- syncCursorQuestionToolForModel(pi, event.model);
246
- });
247
234
  }
@@ -10,6 +10,48 @@ export function hasUsableText(value: string | undefined): value is string {
10
10
  return typeof value === "string" && value.trim().length > 0;
11
11
  }
12
12
 
13
+ export function getString(record: Record<string, unknown> | undefined, key: string): string | undefined {
14
+ const value = record?.[key];
15
+ return typeof value === "string" ? value : undefined;
16
+ }
17
+
18
+ export function getNumber(record: Record<string, unknown> | undefined, key: string): number | undefined {
19
+ const value = record?.[key];
20
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
21
+ }
22
+
23
+ export function getBoolean(record: Record<string, unknown> | undefined, key: string): boolean | undefined {
24
+ const value = record?.[key];
25
+ return typeof value === "boolean" ? value : undefined;
26
+ }
27
+
28
+ export function getRecord(record: Record<string, unknown> | undefined, key: string): Record<string, unknown> | undefined {
29
+ return asRecord(record?.[key]);
30
+ }
31
+
32
+ export function getArray(record: Record<string, unknown> | undefined, key: string): unknown[] | undefined {
33
+ const value = record?.[key];
34
+ return Array.isArray(value) ? value : undefined;
35
+ }
36
+
37
+ export function firstNonEmptyString(...values: Array<string | undefined>): string | undefined {
38
+ for (const value of values) {
39
+ const trimmed = value?.trim();
40
+ if (trimmed) return trimmed;
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ export function stringifyUnknown(value: unknown, options: { pretty?: boolean } = {}): string {
46
+ if (value === undefined) return "";
47
+ if (typeof value === "string") return value;
48
+ try {
49
+ return JSON.stringify(value, null, options.pretty ? 2 : undefined) ?? String(value);
50
+ } catch {
51
+ return String(value);
52
+ }
53
+ }
54
+
13
55
  export function getFirstStringByKeys(
14
56
  record: Record<string, unknown> | undefined,
15
57
  keys: readonly string[],