pi-cursor-sdk 0.1.40 → 0.1.42

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 (43) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +12 -9
  3. package/docs/cursor-dogfood-checklist.md +6 -0
  4. package/docs/cursor-live-smoke-checklist.md +4 -4
  5. package/docs/cursor-model-ux-spec.md +6 -6
  6. package/docs/cursor-native-tool-replay.md +11 -7
  7. package/docs/cursor-native-tool-visual-audit.md +2 -2
  8. package/docs/cursor-testing-lessons.md +1 -1
  9. package/docs/cursor-tool-surfaces.md +4 -0
  10. package/docs/platform-smoke.md +9 -1
  11. package/package.json +8 -5
  12. package/scripts/lib/cursor-visual-manifest.d.mts +3 -0
  13. package/scripts/lib/cursor-visual-manifest.mjs +82 -0
  14. package/scripts/platform-smoke/artifacts.mjs +147 -2
  15. package/scripts/platform-smoke/card-detect.mjs +1 -1
  16. package/scripts/platform-smoke/doctor.mjs +53 -8
  17. package/scripts/platform-smoke/scenarios.mjs +1 -1
  18. package/scripts/platform-smoke.mjs +69 -7
  19. package/scripts/visual-tui-smoke-self-test.mjs +229 -0
  20. package/scripts/visual-tui-smoke.mjs +45 -179
  21. package/src/context.ts +25 -10
  22. package/src/cursor-active-tools.ts +7 -0
  23. package/src/cursor-compact-tool-summary.ts +81 -0
  24. package/src/cursor-native-tool-display-registration.ts +31 -21
  25. package/src/cursor-native-tool-display-replay.ts +13 -2
  26. package/src/cursor-native-tool-display-state.ts +13 -4
  27. package/src/cursor-pi-tool-bridge-run.ts +6 -3
  28. package/src/cursor-pi-tool-bridge-types.ts +2 -2
  29. package/src/cursor-provider-errors.ts +2 -1
  30. package/src/cursor-provider-live-run-drain.ts +1 -1
  31. package/src/cursor-provider-turn-prepare.ts +1 -1
  32. package/src/cursor-provider-turn-send.ts +2 -0
  33. package/src/cursor-question-tool.ts +2 -1
  34. package/src/cursor-replay-activity-builders.ts +12 -4
  35. package/src/cursor-replay-summary-args.ts +21 -2
  36. package/src/cursor-sdk-event-debug.ts +3 -1
  37. package/src/cursor-skill-tool.ts +2 -1
  38. package/src/cursor-task-presentation.ts +77 -0
  39. package/src/cursor-tool-manifest.ts +2 -1
  40. package/src/cursor-tool-presentation-registry.ts +16 -2
  41. package/src/cursor-tool-result-display-readers.ts +13 -8
  42. package/src/cursor-transcript-tool-formatters.ts +5 -5
  43. package/src/cursor-usage-accounting.ts +5 -4
@@ -150,7 +150,8 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
150
150
  this.debugRecorder = recorder;
151
151
  }
152
152
 
153
- resolveToolResults(toolResults: readonly ToolResultMessage[]): void {
153
+ async resolveToolResults(toolResults: readonly ToolResultMessage[]): Promise<void> {
154
+ let resolvedCount = 0;
154
155
  for (const toolResult of toolResults) {
155
156
  const pending = this.pendingByPiToolCallId.get(toolResult.toolCallId);
156
157
  if (!pending || pending.settled) continue;
@@ -158,11 +159,13 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
158
159
  content: convertPiContentToMcpContent(toolResult.content),
159
160
  isError: toolResult.isError || undefined,
160
161
  });
162
+ resolvedCount += 1;
161
163
  }
164
+ if (resolvedCount > 0) await waitForProtocolFlush();
162
165
  }
163
166
 
164
- resolveToolResultsFromContext(context: Context): void {
165
- this.resolveToolResults(context.messages.map(asToolResultMessage).filter((message): message is ToolResultMessage => message !== undefined));
167
+ async resolveToolResultsFromContext(context: Context): Promise<void> {
168
+ await this.resolveToolResults(context.messages.map(asToolResultMessage).filter((message): message is ToolResultMessage => message !== undefined));
166
169
  }
167
170
 
168
171
  hasPendingPiToolCallId(piToolCallId: string): boolean {
@@ -61,8 +61,8 @@ export interface CursorPiToolBridgeRun {
61
61
  mcpServers?: Record<string, McpServerConfig>;
62
62
  snapshot: CursorPiToolBridgeSnapshot;
63
63
  takeQueuedToolRequests(): CursorPiBridgeToolRequest[];
64
- resolveToolResults(toolResults: readonly ToolResultMessage[]): void;
65
- resolveToolResultsFromContext(context: Context): void;
64
+ resolveToolResults(toolResults: readonly ToolResultMessage[]): Promise<void>;
65
+ resolveToolResultsFromContext(context: Context): Promise<void>;
66
66
  hasPendingPiToolCallId(piToolCallId: string): boolean;
67
67
  isBridgeMcpToolCall(toolCall: unknown): boolean;
68
68
  setOnToolRequest(handler?: (request: CursorPiBridgeToolRequest) => void): void;
@@ -15,7 +15,7 @@ const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
15
15
  // Keep this phrase aligned with pi's agent-level retry classifier (`provider.?returned.?error`).
16
16
  const RETRYABLE_CURSOR_RUN_FAILURE_PREFIX = "Provider returned error: Cursor SDK run failed";
17
17
 
18
- export type CursorSdkRunFailureSource = Pick<RunResult, "id" | "status" | "durationMs" | "model" | "result">;
18
+ export type CursorSdkRunFailureSource = Pick<RunResult, "id" | "requestId" | "status" | "durationMs" | "model" | "result">;
19
19
 
20
20
  function isGenericErrorMessage(message: string): boolean {
21
21
  const normalized = message.trim().toLowerCase();
@@ -159,6 +159,7 @@ export function formatCursorSdkRunFailureDetail(result: CursorSdkRunFailureSourc
159
159
  const parts = [RETRYABLE_CURSOR_RUN_FAILURE_PREFIX];
160
160
  if (result.model?.id) parts.push(`model ${result.model.id}`);
161
161
  parts.push(`run ${shortRunId(result.id)}`);
162
+ if (result.requestId) parts.push(`request ${shortRunId(result.requestId)}`);
162
163
  if (typeof result.durationMs === "number") parts.push(`${result.durationMs}ms`);
163
164
  return parts.join(" · ");
164
165
  }
@@ -414,7 +414,7 @@ export async function drainExistingCursorLiveRunBeforeSend(
414
414
  const outcome = await cursorLiveRuns.withRunLease(run, signal, async () => {
415
415
  if (run.disposed) return "continue_send" as const;
416
416
  const consumed = cursorLiveRuns.consumeToolResults(run, context, getCursorNativeReplayIdFromToolCallId);
417
- run.bridgeRun?.resolveToolResults(consumed.toolResults);
417
+ await run.bridgeRun?.resolveToolResults(consumed.toolResults);
418
418
  const shouldChainUserInput = run.chainUserInputAfterCompletion || hasTrailingUserMessagesAfterToolResults(context);
419
419
  if (shouldChainUserInput) run.chainUserInputAfterCompletion = true;
420
420
  while (!cursorLiveRuns.isReady(run)) {
@@ -89,7 +89,7 @@ export async function prepareCursorProviderTurn(
89
89
  throwIfAborted();
90
90
 
91
91
  const buildPromptOptions = (plan: ReturnType<typeof planCursorSessionSend>) => {
92
- const promptOptions = getCursorPromptOptions(model);
92
+ const promptOptions = { ...getCursorPromptOptions(model), agentMode };
93
93
  if (plan.mode !== "bootstrap" || !resolveCursorToolManifestEnabled()) {
94
94
  return promptOptions;
95
95
  }
@@ -78,12 +78,14 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
78
78
  sdkRun = run;
79
79
  sdkEventDebug?.recordRunMeta({
80
80
  runId: run.id,
81
+ requestId: run.requestId,
81
82
  agentId: run.agentId,
82
83
  status: run.status,
83
84
  });
84
85
  sdkEventDebug?.attachRunStream(run);
85
86
  sdkEventDebug?.recordProviderEvent("agent_send_returned", {
86
87
  runId: run.id,
88
+ requestId: run.requestId,
87
89
  agentId: run.agentId,
88
90
  status: run.status,
89
91
  });
@@ -1,6 +1,7 @@
1
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
+ import { arePiToolsDisabled } from "./cursor-active-tools.js";
4
5
  import { isCursorModel } from "./cursor-model.js";
5
6
  import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
6
7
  import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
@@ -175,7 +176,7 @@ async function askOneQuestion(question: CursorQuestion, ctx: { ui: ExtensionCont
175
176
 
176
177
  function syncCursorQuestionToolForModel(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">, model: ExtensionContext["model"]): void {
177
178
  const activeToolNames = new Set(pi.getActiveTools());
178
- const shouldBeActive = isCursorModel(model) && resolveCursorPiToolBridgeEnabled();
179
+ const shouldBeActive = !arePiToolsDisabled(pi) && isCursorModel(model) && resolveCursorPiToolBridgeEnabled();
179
180
  const alreadyActive = activeToolNames.has(CURSOR_ASK_QUESTION_TOOL_NAME);
180
181
  if (shouldBeActive === alreadyActive) return;
181
182
  if (shouldBeActive) {
@@ -21,12 +21,12 @@ import {
21
21
  readMcpDisplayResult,
22
22
  getReadLintDiagnostics,
23
23
  getReadLintPaths,
24
- getTaskDescription,
25
24
  getTodoItems,
26
25
  getTodoTotalCount,
27
26
  inferImageMimeType,
28
27
  } from "./cursor-tool-result-display-readers.js";
29
28
  import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-args.js";
29
+ import { formatCursorTaskAgentId, formatCursorTaskKind, getCursorTaskDescription, getCursorTaskPresentationMode, readCursorTaskMetadata } from "./cursor-task-presentation.js";
30
30
 
31
31
  export interface CursorReplayActivityBuildContext {
32
32
  args: Record<string, unknown>;
@@ -88,12 +88,20 @@ export function buildCreatePlanReplaySummaryArgs({ args, result }: CursorReplayA
88
88
  };
89
89
  }
90
90
 
91
- export function buildTaskReplaySummaryArgs({ args, result }: CursorReplayActivityBuildContext): CursorReplayTaskSummaryArgs {
92
- const description = getTaskDescription(args, result);
93
- const preview = firstNonEmptyLine(collectTaskText(result));
91
+ export function buildTaskReplaySummaryArgs({ args, result, options }: CursorReplayActivityBuildContext): CursorReplayTaskSummaryArgs {
92
+ const description = getCursorTaskDescription(args, result.value);
93
+ const preview = firstNonEmptyLine(collectTaskText(result, options));
94
+ const metadata = readCursorTaskMetadata(args, result.value);
95
+ const displayAgentId = formatCursorTaskAgentId(metadata.agentId);
96
+ const includeMetadata = getCursorTaskPresentationMode() === "subagent-meta";
94
97
  return {
95
98
  description: truncateArg(description),
96
99
  ...(preview ? { preview: truncateArg(preview) } : {}),
100
+ ...(includeMetadata && metadata.subagentName ? { subagentName: truncateArg(metadata.subagentName) } : {}),
101
+ ...(includeMetadata && metadata.subagentKind ? { subagentKind: truncateArg(formatCursorTaskKind(metadata.subagentKind) ?? metadata.subagentKind) } : {}),
102
+ ...(includeMetadata && metadata.model ? { model: truncateArg(metadata.model) } : {}),
103
+ ...(includeMetadata && displayAgentId ? { agentId: truncateArg(displayAgentId) } : {}),
104
+ ...(includeMetadata && metadata.isBackground === true ? { isBackground: true } : {}),
97
105
  };
98
106
  }
99
107
 
@@ -31,6 +31,11 @@ export interface CursorReplayPlanSummaryArgs extends CursorReplayTodoSummaryArgs
31
31
  export interface CursorReplayTaskSummaryArgs extends CursorReplayActivitySummaryOverride {
32
32
  description?: string;
33
33
  preview?: string;
34
+ subagentName?: string;
35
+ subagentKind?: string;
36
+ model?: string;
37
+ agentId?: string;
38
+ isBackground?: boolean;
34
39
  }
35
40
 
36
41
  export interface CursorReplayGenerateImageSummaryArgs extends CursorReplayActivitySummaryOverride {
@@ -150,8 +155,22 @@ export function summarizeReplayPlan(args: CursorReplayPlanSummaryArgs | undefine
150
155
  export function summarizeReplayTask(args: CursorReplayTaskSummaryArgs | undefined): string | undefined {
151
156
  const description = readCursorReplaySummaryString(args, "description");
152
157
  const preview = readCursorReplaySummaryString(args, "preview");
153
- if (description && preview && preview !== description) return `${description}: ${preview}`;
154
- return description ?? preview;
158
+ const subagentName = readCursorReplaySummaryString(args, "subagentName");
159
+ const subagentKind = readCursorReplaySummaryString(args, "subagentKind");
160
+ const model = readCursorReplaySummaryString(args, "model");
161
+ const agentId = readCursorReplaySummaryString(args, "agentId");
162
+ const metadataParts = [
163
+ subagentKind,
164
+ model,
165
+ agentId ? `ID: ${agentId}` : undefined,
166
+ args?.isBackground === true ? "backgrounded" : undefined,
167
+ ].filter((part): part is string => Boolean(part));
168
+ const subjectParts = [description].filter((part): part is string => Boolean(part));
169
+ const subject = subjectParts.length > 0 ? subjectParts.join(" · ") : undefined;
170
+ const head = metadataParts.length > 0 ? [subject, ...metadataParts].filter(Boolean).join(" · ") : subject;
171
+ if (metadataParts.length > 0) return head;
172
+ if (head && preview && preview !== description && preview !== subagentName) return `${head}: ${preview}`;
173
+ return head ?? preview;
155
174
  }
156
175
 
157
176
  export function summarizeReplayMcp(args: CursorReplayMcpSummaryArgs | undefined): string | undefined {
@@ -78,12 +78,14 @@ export interface CursorSdkEventDebugSendMeta {
78
78
 
79
79
  export interface CursorSdkEventDebugRunMeta {
80
80
  runId: string;
81
+ requestId?: string;
81
82
  agentId: string;
82
83
  status: string;
83
84
  }
84
85
 
85
86
  interface CursorSdkRunLike {
86
87
  id: string;
88
+ requestId?: string;
87
89
  agentId?: string;
88
90
  status?: string;
89
91
  stream?: () => AsyncIterable<unknown>;
@@ -373,7 +375,7 @@ export class CursorSdkEventDebugSink {
373
375
  attachRunStream(run: unknown): void {
374
376
  const sdkRun = run as CursorSdkRunLike;
375
377
  if (typeof sdkRun.stream !== "function") {
376
- this.recordProviderEvent("run_stream_unavailable", { runId: sdkRun.id });
378
+ this.recordProviderEvent("run_stream_unavailable", { runId: sdkRun.id, requestId: sdkRun.requestId });
377
379
  return;
378
380
  }
379
381
  this.streamCapturePromise = (async () => {
@@ -8,6 +8,7 @@ import type {
8
8
  Skill,
9
9
  } from "@earendil-works/pi-coding-agent";
10
10
  import { Type } from "typebox";
11
+ import { arePiToolsDisabled } from "./cursor-active-tools.js";
11
12
  import { isCursorModel } from "./cursor-model.js";
12
13
  import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
13
14
  import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
@@ -62,7 +63,7 @@ function shouldExposeSkillTool(model: ExtensionContext["model"]): boolean {
62
63
 
63
64
  function syncCursorSkillToolForModel(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">, model: ExtensionContext["model"]): void {
64
65
  const activeToolNames = new Set(pi.getActiveTools());
65
- const shouldBeActive = shouldExposeSkillTool(model);
66
+ const shouldBeActive = !arePiToolsDisabled(pi) && shouldExposeSkillTool(model);
66
67
  const alreadyActive = activeToolNames.has(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
67
68
  if (shouldBeActive === alreadyActive) return;
68
69
  if (shouldBeActive) {
@@ -0,0 +1,77 @@
1
+ import { asRecord, getBoolean, getRecord, getString } from "./cursor-record-utils.js";
2
+
3
+ export const CURSOR_TASK_PRESENTATION_ENV = "PI_CURSOR_TASK_PRESENTATION";
4
+
5
+ export type CursorTaskPresentationMode = "task" | "subagent" | "subagent-meta";
6
+
7
+ const VALID_CURSOR_TASK_PRESENTATION_MODES = new Set<CursorTaskPresentationMode>([
8
+ "task",
9
+ "subagent",
10
+ "subagent-meta",
11
+ ]);
12
+
13
+ export function getCursorTaskPresentationMode(env: NodeJS.ProcessEnv = process.env): CursorTaskPresentationMode {
14
+ const raw = env[CURSOR_TASK_PRESENTATION_ENV]?.trim();
15
+ return raw && VALID_CURSOR_TASK_PRESENTATION_MODES.has(raw as CursorTaskPresentationMode)
16
+ ? (raw as CursorTaskPresentationMode)
17
+ : "subagent-meta";
18
+ }
19
+
20
+ export interface CursorTaskMetadata {
21
+ description?: string;
22
+ subagentKind?: string;
23
+ subagentName?: string;
24
+ model?: string;
25
+ agentId?: string;
26
+ isBackground?: boolean;
27
+ }
28
+
29
+ export function getCursorTaskDescription(args: Record<string, unknown>, resultValue?: unknown): string {
30
+ return getString(args, "description") ?? getString(asRecord(resultValue), "description") ?? "task";
31
+ }
32
+
33
+ export function readCursorTaskMetadata(args: Record<string, unknown>, resultValue?: unknown): CursorTaskMetadata {
34
+ const subagentType = getRecord(args, "subagentType");
35
+ const result = asRecord(resultValue);
36
+ return {
37
+ description: getCursorTaskDescription(args, resultValue),
38
+ subagentKind: getString(subagentType, "kind"),
39
+ subagentName: getString(subagentType, "name"),
40
+ model: getString(args, "model"),
41
+ agentId: getString(args, "agentId") ?? getString(result, "agentId"),
42
+ isBackground: getBoolean(result, "isBackground"),
43
+ };
44
+ }
45
+
46
+ function cleanMetadataValue(value: string | undefined): string | undefined {
47
+ const trimmed = value?.trim();
48
+ return trimmed || undefined;
49
+ }
50
+
51
+ export function getCursorTaskActivityTitle(): string {
52
+ return getCursorTaskPresentationMode() === "task" ? "Cursor task" : "Cursor subagent";
53
+ }
54
+
55
+ export function getCursorTaskTranscriptHeader(args: Record<string, unknown>, resultValue?: unknown): string {
56
+ const metadata = readCursorTaskMetadata(args, resultValue);
57
+ const description = cleanMetadataValue(metadata.description) ?? "task";
58
+ const mode = getCursorTaskPresentationMode();
59
+ if (mode === "task") return `task ${description}`;
60
+ if (mode === "subagent-meta") {
61
+ const subagentName = cleanMetadataValue(metadata.subagentName);
62
+ return subagentName ? `subagent ${subagentName} ${description}` : `subagent ${description}`;
63
+ }
64
+ return `subagent ${description}`;
65
+ }
66
+
67
+ export function formatCursorTaskKind(value: string | undefined): string | undefined {
68
+ const cleaned = cleanMetadataValue(value);
69
+ if (!cleaned) return undefined;
70
+ return cleaned.slice(0, 1).toUpperCase() + cleaned.slice(1);
71
+ }
72
+
73
+ export function formatCursorTaskAgentId(value: string | undefined): string | undefined {
74
+ const cleaned = cleanMetadataValue(value);
75
+ if (!cleaned || !/^[A-Za-z0-9_.:-]+$/.test(cleaned)) return undefined;
76
+ return cleaned.length > 12 ? cleaned.slice(0, 8) : cleaned;
77
+ }
@@ -4,7 +4,7 @@ import type { CursorPiToolBridgeSnapshot } from "./cursor-pi-tool-bridge-types.j
4
4
  export const CURSOR_TOOL_MANIFEST_ENV = "PI_CURSOR_TOOL_MANIFEST";
5
5
 
6
6
  /**
7
- * Representative @cursor/sdk@1.0.17 local-agent ToolType values; actual exposure can vary by run.
7
+ * Representative @cursor/sdk@1.0.18 local-agent ToolType values; actual exposure can vary by run.
8
8
  * See docs/cursor-native-tool-replay.md#sdk-tooltype-replay-matrix.
9
9
  */
10
10
  export const CURSOR_HOST_TOOL_MANIFEST_SUMMARY =
@@ -26,6 +26,7 @@ export function buildCursorToolManifestText(options: {
26
26
  "Callable tool surfaces this run:",
27
27
  `- Cursor SDK host tools (callable; not listed in MCP listTools): ${CURSOR_HOST_TOOL_MANIFEST_SUMMARY}.`,
28
28
  "- Configured Cursor MCP servers: discovered at runtime via MCP listTools (depends on Cursor settings and PI_CURSOR_SETTING_SOURCES).",
29
+ "- Pi CLI tool toggles such as --no-tools affect pi tools and bridge exposure only; they do not disable Cursor SDK host tools or configured Cursor MCP.",
29
30
  ];
30
31
  const bridgeTools = options.bridgeSnapshot?.tools ?? [];
31
32
  if (!piBridgeEnabled) {
@@ -23,6 +23,7 @@ import {
23
23
  type CursorReplayWebSearchSummaryArgs,
24
24
  } from "./cursor-replay-summary-args.js";
25
25
  import { truncateCursorDisplayLine } from "./cursor-display-text.js";
26
+ import { getCursorTaskActivityTitle } from "./cursor-task-presentation.js";
26
27
  import {
27
28
  buildCreatePlanReplaySummaryArgs,
28
29
  buildDeleteReplayDetailFields,
@@ -75,6 +76,10 @@ export interface CursorToolVisibilityPolicy {
75
76
  fastLocalDiscovery?: boolean;
76
77
  }
77
78
 
79
+ export interface CursorToolReplayDisplayPolicy {
80
+ showCollapsedExpandHint?: boolean;
81
+ }
82
+
78
83
  export interface CursorToolActivityReplaySpec<TArgs extends CursorReplaySummaryArgs = CursorReplaySummaryArgs> {
79
84
  buildActivityArgs: (context: CursorReplayActivityBuildContext) => TArgs;
80
85
  buildDetails: (context: CursorReplayActivityBuildContext, contentText: string) => CursorReplayActivityDetailFields;
@@ -90,7 +95,9 @@ export interface CursorToolPresentationSpec {
90
95
  /** Raw SDK/host names that resolve to this tool via {@link normalizeCursorToolName}. */
91
96
  nameAliases?: readonly string[];
92
97
  displayLabel: string;
98
+ getActivityTitle?: () => string;
93
99
  visibility: CursorToolVisibilityPolicy;
100
+ replayDisplay?: CursorToolReplayDisplayPolicy;
94
101
  webKind?: CursorWebToolKind;
95
102
  /** Regexes matched against lowercased trimmed tool names for {@link classifyCursorWebToolKind}. */
96
103
  webNamePatterns?: readonly RegExp[];
@@ -209,8 +216,10 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
209
216
  },
210
217
  {
211
218
  normalizedName: "task",
212
- displayLabel: "Cursor task",
219
+ displayLabel: "Cursor subagent",
220
+ getActivityTitle: getCursorTaskActivityTitle,
213
221
  visibility: { lifecycleEligible: true },
222
+ replayDisplay: { showCollapsedExpandHint: true },
214
223
  lifecycleLabelKind: "task",
215
224
  replayCallSummary: withActivitySummaryFallback(summarizeReplayTask),
216
225
  activityReplay: {
@@ -381,7 +390,7 @@ export function getCursorReplayPromptLabel(toolName: string): string {
381
390
  export function getCursorReplayActivityTitle(toolName: string): string | undefined {
382
391
  const spec = getCursorToolPresentationSpec(toolName);
383
392
  if (!spec || !hasNeutralActivityTitle(spec)) return undefined;
384
- return spec.displayLabel;
393
+ return spec.getActivityTitle?.() ?? spec.displayLabel;
385
394
  }
386
395
 
387
396
  function buildCursorGenericActivityTitle(displayName: string): string {
@@ -418,6 +427,11 @@ export function getCursorToolGenerateImageReplaySpec(normalizedKey: string): Cur
418
427
  return getCursorToolPresentationSpecByNormalizedKey(normalizedKey)?.generateImageReplay;
419
428
  }
420
429
 
430
+ export function shouldShowCursorReplayCollapsedExpandHint(normalizedKey: string | undefined): boolean {
431
+ if (!normalizedKey) return false;
432
+ return getCursorToolPresentationSpecByNormalizedKey(normalizedKey)?.replayDisplay?.showCollapsedExpandHint === true;
433
+ }
434
+
421
435
  export function getCursorReplayCallSummary(
422
436
  toolName: CursorReplayToolName | CursorReplaySourceToolName,
423
437
  args: CursorReplaySummaryArgs | undefined,
@@ -1,4 +1,5 @@
1
1
  import { asRecord, getArray, getNumber, getRecord, getString, stringifyUnknown } from "./cursor-record-utils.js";
2
+ import { summarizeCursorCompactConversationToolCall } from "./cursor-compact-tool-summary.js";
2
3
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
3
4
  import { firstNonEmptyLine, formatDisplayPath, truncateArg } from "./cursor-transcript-utils.js";
4
5
 
@@ -70,10 +71,6 @@ export function getTodoTotalCount(args: Record<string, unknown>, result: CursorT
70
71
  return getNumber(asRecord(result.value), "totalCount") ?? getNumber(args, "totalCount") ?? todos.length;
71
72
  }
72
73
 
73
- export function getTaskDescription(args: Record<string, unknown>, result: CursorToolResultLike): string {
74
- return getString(args, "description") ?? getString(asRecord(result.value), "description") ?? "task";
75
- }
76
-
77
74
  function getNestedRecord(record: Record<string, unknown> | undefined, ...keys: string[]): Record<string, unknown> | undefined {
78
75
  let current = record;
79
76
  for (const key of keys) {
@@ -83,16 +80,24 @@ function getNestedRecord(record: Record<string, unknown> | undefined, ...keys: s
83
80
  return current;
84
81
  }
85
82
 
86
- export function collectTaskText(result: CursorToolResultLike): string {
83
+ function readConversationStepAssistantText(step: unknown): string | undefined {
84
+ const record = asRecord(step);
85
+ const legacyText = getString(getRecord(record, "assistantMessage"), "text");
86
+ if (legacyText) return legacyText;
87
+ if (getString(record, "type") !== "assistantMessage") return undefined;
88
+ return getString(getRecord(record, "message"), "text");
89
+ }
90
+
91
+ export function collectTaskText(result: CursorToolResultLike, options: CursorToolResultReaderOptions = {}): string {
87
92
  const value = asRecord(result.value);
88
93
  const success = getNestedRecord(value, "result", "success");
89
94
  const command = getString(success, "command");
90
95
  const stdout = getString(success, "stdout");
91
96
  const interleavedOutput = getString(success, "interleavedOutput");
92
- const assistantMessages = (getArray(value, "conversationSteps") ?? [])
93
- .map((step) => getString(getRecord(asRecord(step), "assistantMessage"), "text"))
97
+ const conversationParts = (getArray(value, "conversationSteps") ?? [])
98
+ .map((step) => summarizeCursorCompactConversationToolCall(step, options) ?? readConversationStepAssistantText(step))
94
99
  .filter((entry): entry is string => Boolean(entry));
95
- const parts = [command ? `$ ${command}` : undefined, stdout || interleavedOutput, ...assistantMessages].filter((part): part is string => Boolean(part));
100
+ const parts = [command ? `$ ${command}` : undefined, stdout || interleavedOutput, ...conversationParts].filter((part): part is string => Boolean(part));
96
101
  return parts.join("\n");
97
102
  }
98
103
 
@@ -9,13 +9,13 @@ import {
9
9
  getRecord,
10
10
  getString,
11
11
  } from "./cursor-record-utils.js";
12
+ import { getCursorTaskTranscriptHeader } from "./cursor-task-presentation.js";
12
13
  import {
13
14
  collectTaskText,
14
15
  getGenerateImageDisplayPath,
15
16
  readMcpDisplayResult,
16
17
  getReadLintDiagnostics,
17
18
  getReadLintPaths,
18
- getTaskDescription,
19
19
  getTodoItems,
20
20
  } from "./cursor-tool-result-display-readers.js";
21
21
 
@@ -487,10 +487,10 @@ export function formatPlan(args: Record<string, unknown>, result: NormalizedResu
487
487
  }
488
488
 
489
489
  export function formatTask(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
490
- const description = getTaskDescription(args, result);
491
- if (result.status === "error") return joinSections(`task ${description}`, formatError(result.error));
492
- const taskText = collectTaskText(result);
493
- return joinSections(`task ${description}`, limitText(taskText || stringifyUnknown(result.value), options));
490
+ const header = getCursorTaskTranscriptHeader(args, result.value);
491
+ if (result.status === "error") return joinSections(header, formatError(result.error));
492
+ const taskText = collectTaskText(result, options);
493
+ return joinSections(header, limitText(taskText || stringifyUnknown(result.value), options));
494
494
  }
495
495
 
496
496
  export function formatGenerateImage(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
@@ -56,10 +56,11 @@ export function estimateCursorContextTotalTokens(partial: AssistantMessage, mode
56
56
  return estimateCursorContextTokens(withAssistantMessage(context, partial), getCursorPromptOptions(model));
57
57
  }
58
58
 
59
- export function applyCursorApproximateUsage(partial: AssistantMessage, model: Model<Api>, context: Context, sessionInputTokens: number): void {
60
- partial.usage.input = sessionInputTokens;
61
- partial.usage.output = estimateCursorAssistantSessionOutputTokens(partial);
59
+ export function applyCursorApproximateUsage(partial: AssistantMessage, _model: Model<Api>, _context: Context, sessionInputTokens: number): void {
60
+ const outputTokens = estimateCursorAssistantSessionOutputTokens(partial);
61
+ partial.usage.input = Math.max(0, sessionInputTokens);
62
+ partial.usage.output = outputTokens;
62
63
  partial.usage.cacheRead = 0;
63
64
  partial.usage.cacheWrite = 0;
64
- partial.usage.totalTokens = estimateCursorContextTotalTokens(partial, model, context);
65
+ partial.usage.totalTokens = partial.usage.input + partial.usage.output + partial.usage.cacheRead + partial.usage.cacheWrite;
65
66
  }