pi-cursor-sdk 0.1.40 → 0.1.41
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 +14 -0
- package/README.md +10 -9
- package/docs/cursor-dogfood-checklist.md +6 -0
- package/docs/cursor-live-smoke-checklist.md +4 -4
- package/docs/cursor-model-ux-spec.md +6 -6
- package/docs/cursor-native-tool-replay.md +4 -4
- package/docs/cursor-native-tool-visual-audit.md +2 -2
- package/docs/cursor-testing-lessons.md +1 -1
- package/docs/cursor-tool-surfaces.md +4 -0
- package/docs/platform-smoke.md +9 -1
- package/package.json +8 -5
- package/scripts/lib/cursor-visual-manifest.d.mts +3 -0
- package/scripts/lib/cursor-visual-manifest.mjs +82 -0
- package/scripts/platform-smoke/artifacts.mjs +147 -2
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/doctor.mjs +53 -8
- package/scripts/platform-smoke/scenarios.mjs +1 -1
- package/scripts/platform-smoke.mjs +69 -7
- package/scripts/visual-tui-smoke-self-test.mjs +229 -0
- package/scripts/visual-tui-smoke.mjs +45 -179
- package/src/context.ts +25 -10
- package/src/cursor-active-tools.ts +7 -0
- package/src/cursor-native-tool-display-registration.ts +31 -21
- package/src/cursor-native-tool-display-state.ts +13 -4
- package/src/cursor-pi-tool-bridge-run.ts +6 -3
- package/src/cursor-pi-tool-bridge-types.ts +2 -2
- package/src/cursor-provider-errors.ts +2 -1
- package/src/cursor-provider-live-run-drain.ts +1 -1
- package/src/cursor-provider-turn-prepare.ts +1 -1
- package/src/cursor-provider-turn-send.ts +2 -0
- package/src/cursor-question-tool.ts +2 -1
- package/src/cursor-sdk-event-debug.ts +3 -1
- package/src/cursor-skill-tool.ts +2 -1
- package/src/cursor-tool-manifest.ts +2 -1
- package/src/cursor-usage-accounting.ts +5 -4
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { accessSync,
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
3
|
+
import { accessSync, constants, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
4
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
6
|
import { commonBooleanFlag, commonRepeatStringFlag, parseArgv } from "./lib/cursor-cli-args.mjs";
|
|
8
7
|
import { buildCursorSmokeEnvPlan, CURSOR_SDK_EVENT_DEBUG_ENV_NAMES, sealedNodePath } from "./lib/cursor-smoke-env.mjs";
|
|
8
|
+
import { writeVisualManifest } from "./lib/cursor-visual-manifest.mjs";
|
|
9
|
+
import { runVisualSmokeSelfTest } from "./visual-tui-smoke-self-test.mjs";
|
|
9
10
|
import { buildTerminalHtml, writeTerminalScreenshot } from "./lib/cursor-visual-render.mjs";
|
|
10
11
|
|
|
11
12
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -71,6 +72,7 @@ Artifacts written:
|
|
|
71
72
|
<label>.html Self-contained browser/xterm render.
|
|
72
73
|
<label>.png Browser-rendered screenshot, unless --no-screenshot.
|
|
73
74
|
<label>.jsonl.path Latest persisted pi session JSONL path.
|
|
75
|
+
<label>.manifest.json Agent-readable artifact index for this run.
|
|
74
76
|
|
|
75
77
|
Prerequisites:
|
|
76
78
|
- pi, node, tmux, and npm-installed dev dependencies on PATH / in node_modules.
|
|
@@ -448,205 +450,60 @@ function runVisualSmoke(options) {
|
|
|
448
450
|
const htmlPath = `${base}.html`;
|
|
449
451
|
const pngPath = `${base}.png`;
|
|
450
452
|
const jsonlPathFile = `${base}.jsonl.path`;
|
|
453
|
+
const manifestPath = `${base}.manifest.json`;
|
|
451
454
|
|
|
452
455
|
writeUtf8(ansiPath, ansi);
|
|
453
456
|
writeUtf8(textPath, plain);
|
|
454
457
|
writeUtf8(htmlPath, buildTerminalHtml({ ansi, plain, options }));
|
|
455
458
|
|
|
459
|
+
const partialArtifacts = { ansiPath, textPath, htmlPath, pngPath, jsonlPathFile, manifestPath };
|
|
456
460
|
const jsonlPath = findLatestJsonl(options.sessionDir, { sinceMs: runStartedAtMs, previousMtimes: jsonlMtimesBeforeRun });
|
|
457
|
-
if (!jsonlPath)
|
|
461
|
+
if (!jsonlPath) {
|
|
462
|
+
const message = `no current-run persisted .jsonl found under ${options.sessionDir}`;
|
|
463
|
+
writeVisualManifest(manifestPath, options, partialArtifacts, { message, writtenAt: new Date().toISOString() });
|
|
464
|
+
throw new Error(message);
|
|
465
|
+
}
|
|
458
466
|
writeUtf8(jsonlPathFile, `${jsonlPath}\n`);
|
|
459
467
|
|
|
460
|
-
return {
|
|
468
|
+
return { ...partialArtifacts, jsonlPath };
|
|
461
469
|
} finally {
|
|
462
470
|
if (bufferLoaded) run(commands.tmux, ["delete-buffer", "-b", bufferName]);
|
|
463
471
|
if (sessionStarted) run(commands.tmux, ["kill-session", "-t", sessionName]);
|
|
464
472
|
}
|
|
465
473
|
}
|
|
466
474
|
|
|
467
|
-
function assertSelfTest(condition, message) {
|
|
468
|
-
if (!condition) throw new Error(`self-test failed: ${message}`);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function envMap(assignments) {
|
|
472
|
-
return new Map(assignments.map(([name, value]) => [name, value]));
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function parseEnvCapture(path) {
|
|
476
|
-
return new Map(
|
|
477
|
-
readFileSync(path, "utf8")
|
|
478
|
-
.split("\n")
|
|
479
|
-
.filter(Boolean)
|
|
480
|
-
.map((line) => {
|
|
481
|
-
const index = line.indexOf("=");
|
|
482
|
-
return index === -1 ? [line, ""] : [line.slice(0, index), line.slice(index + 1)];
|
|
483
|
-
}),
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function runSelfTest() {
|
|
488
|
-
const tempDir = mkdtempSync(join(tmpdir(), "pi-cursor-sdk-visual-self-test-"));
|
|
489
|
-
try {
|
|
490
|
-
const binDir = join(tempDir, "bin");
|
|
491
|
-
mkdirSync(binDir, { recursive: true });
|
|
492
|
-
const fakePi = join(binDir, "pi");
|
|
493
|
-
const fakeNode = join(binDir, "node");
|
|
494
|
-
const fakeNodeMarker = join(tempDir, "fake-node-used");
|
|
495
|
-
const envCapture = join(tempDir, "fake-pi.env");
|
|
496
|
-
writeFileSync(
|
|
497
|
-
fakePi,
|
|
498
|
-
`#!/usr/bin/env node\nconst { writeFileSync } = require("node:fs");\nwriteFileSync(${JSON.stringify(envCapture)}, Object.entries(process.env).map(([key, value]) => key + "=" + (value ?? "")).join("\\n") + "\\n", "utf8");\n`,
|
|
499
|
-
"utf8",
|
|
500
|
-
);
|
|
501
|
-
writeFileSync(fakeNode, `#!/bin/sh\necho fake-node-used > ${shellQuote(fakeNodeMarker)}\nexit 99\n`, "utf8");
|
|
502
|
-
chmodSync(fakePi, 0o755);
|
|
503
|
-
chmodSync(fakeNode, 0o755);
|
|
504
|
-
|
|
505
|
-
const promptFile = join(tempDir, "prompt.txt");
|
|
506
|
-
writeFileSync(promptFile, "file prompt", "utf8");
|
|
507
|
-
assertSelfTest(parseArgs(["--label", "prompt-order", "--prompt-file", promptFile, "--prompt", "inline prompt"]).prompt === "inline prompt", "--prompt should override an earlier --prompt-file");
|
|
508
|
-
assertSelfTest(parseArgs(["--label", "prompt-dash", "--prompt", "--starts-with-dash"]).prompt === "--starts-with-dash", "--prompt should accept dash-prefixed free-form text");
|
|
509
|
-
assertSelfTest(parseArgs(["--label", "prompt-order", "--prompt", "inline prompt", "--prompt-file", promptFile]).prompt === "file prompt", "--prompt-file should override an earlier --prompt");
|
|
510
|
-
|
|
511
|
-
const jsonlDir = join(tempDir, "jsonl-filter");
|
|
512
|
-
mkdirSync(jsonlDir, { recursive: true });
|
|
513
|
-
const staleJsonl = join(jsonlDir, "stale.jsonl");
|
|
514
|
-
const freshJsonl = join(jsonlDir, "fresh.jsonl");
|
|
515
|
-
writeFileSync(staleJsonl, "{}\n", "utf8");
|
|
516
|
-
utimesSync(staleJsonl, new Date(1_000), new Date(1_000));
|
|
517
|
-
const previousJsonlMtimes = snapshotJsonlMtimes(jsonlDir);
|
|
518
|
-
writeFileSync(freshJsonl, "{}\n", "utf8");
|
|
519
|
-
utimesSync(freshJsonl, new Date(3_000), new Date(3_000));
|
|
520
|
-
assertSelfTest(findLatestJsonl(jsonlDir, { sinceMs: 2_000, previousMtimes: previousJsonlMtimes }) === freshJsonl, "JSONL discovery should ignore unchanged stale files before run start");
|
|
521
|
-
assertSelfTest(findLatestJsonl(jsonlDir, { sinceMs: 4_000, previousMtimes: snapshotJsonlMtimes(jsonlDir) }) === undefined, "JSONL discovery should not return stale evidence when current run has no changed JSONL");
|
|
522
|
-
|
|
523
|
-
assertSelfTest(!sealedNodePath(process.execPath, "").includes(delimiter), "empty inherited PATH must not leave an empty PATH segment");
|
|
524
|
-
const hostilePath = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
|
|
525
|
-
const sealedHostilePath = sealedNodePath(process.execPath, hostilePath);
|
|
526
|
-
assertSelfTest(resolveCommand("pi", hostilePath) === fakePi, "direct PATH resolver did not prefer fake PATH head");
|
|
527
|
-
assertSelfTest(requireNode() === process.execPath, "node resolver must use process.execPath");
|
|
528
|
-
assertSelfTest(requireCommand("pi", { envPath: hostilePath, env: { ...process.env, PATH: sealedHostilePath } }) === fakePi, "pi prereq should use sealed PATH when executing the shim");
|
|
529
|
-
assertSelfTest(!existsSync(fakeNodeMarker), "pi prereq should not use hostile fake node");
|
|
530
|
-
|
|
531
|
-
const baseOptions = {
|
|
532
|
-
ext: ROOT,
|
|
533
|
-
cwd: ROOT,
|
|
534
|
-
mode: DEFAULT_MODE,
|
|
535
|
-
model: DEFAULT_MODEL,
|
|
536
|
-
outDir: tempDir,
|
|
537
|
-
safeLabel: "self-test",
|
|
538
|
-
sessionDir: join(tempDir, "session"),
|
|
539
|
-
sessionId: "self-test",
|
|
540
|
-
settingSources: DEFAULT_SETTING_SOURCES,
|
|
541
|
-
bridge: false,
|
|
542
|
-
exposeBuiltinTools: false,
|
|
543
|
-
eventDebug: false,
|
|
544
|
-
};
|
|
545
|
-
const plan = buildLaunchPlan(baseOptions, { pi: fakePi, node: process.execPath, sealedPath: sealedHostilePath }, "/bin/sh");
|
|
546
|
-
const defaults = envMap(plan.envAssignments);
|
|
547
|
-
assertSelfTest(defaults.get("PI_CURSOR_NATIVE_TOOL_DISPLAY") === "1", "native display must be forced on");
|
|
548
|
-
assertSelfTest(defaults.get("PI_CURSOR_REGISTER_NATIVE_TOOLS") === "1", "native tool registration must be forced on");
|
|
549
|
-
assertSelfTest(defaults.get("PI_CURSOR_SETTING_SOURCES") === "none", "setting sources must default to none");
|
|
550
|
-
assertSelfTest(defaults.get("PI_CURSOR_PI_TOOL_BRIDGE") === "0", "bridge must default off");
|
|
551
|
-
assertSelfTest(defaults.get("PI_CURSOR_EXPOSE_BUILTIN_TOOLS") === "0", "built-in exposure must default off");
|
|
552
|
-
for (const name of DEBUG_ENV_NAMES) {
|
|
553
|
-
assertSelfTest(plan.clearEnvNames.includes(name), `${name} must be cleared by default`);
|
|
554
|
-
}
|
|
555
|
-
assertSelfTest(plan.script.includes(shellQuote(fakePi)), "launch script must use resolved pi path");
|
|
556
|
-
assertSelfTest(!plan.script.includes(" exec pi "), "launch script must not use bare pi");
|
|
557
|
-
const hostileEnv = {
|
|
558
|
-
...process.env,
|
|
559
|
-
...Object.fromEntries(DEBUG_ENV_NAMES.map((name) => [name, join(tempDir, name)])),
|
|
560
|
-
PATH: hostilePath,
|
|
561
|
-
PI_CURSOR_REGISTER_NATIVE_TOOLS: "0",
|
|
562
|
-
PI_CURSOR_SETTING_SOURCES: "all",
|
|
563
|
-
PI_CURSOR_PI_TOOL_BRIDGE: "1",
|
|
564
|
-
PI_CURSOR_EXPOSE_BUILTIN_TOOLS: "1",
|
|
565
|
-
};
|
|
566
|
-
const probe = run("/bin/sh", ["-c", plan.script], { env: hostileEnv });
|
|
567
|
-
assertSelfTest(probe.status === 0, `fake-pi env capture exited ${probe.status}: ${probe.stderr?.toString() ?? ""}`);
|
|
568
|
-
const capturedEnv = parseEnvCapture(envCapture);
|
|
569
|
-
assertSelfTest(!existsSync(fakeNodeMarker), "launch PATH should force the resolved node before hostile fake node");
|
|
570
|
-
assertSelfTest((capturedEnv.get("PATH") ?? "").split(delimiter)[0] === dirname(process.execPath), "captured PATH should start with resolved node directory");
|
|
571
|
-
assertSelfTest(capturedEnv.get("PI_CURSOR_NATIVE_TOOL_DISPLAY") === "1", "captured env should force native display on");
|
|
572
|
-
assertSelfTest(capturedEnv.get("PI_CURSOR_REGISTER_NATIVE_TOOLS") === "1", "captured env should force native registration on");
|
|
573
|
-
assertSelfTest(capturedEnv.get("PI_CURSOR_SETTING_SOURCES") === "none", "captured env should force settings off");
|
|
574
|
-
assertSelfTest(capturedEnv.get("PI_CURSOR_PI_TOOL_BRIDGE") === "0", "captured env should force bridge off");
|
|
575
|
-
assertSelfTest(capturedEnv.get("PI_CURSOR_EXPOSE_BUILTIN_TOOLS") === "0", "captured env should force built-in exposure off");
|
|
576
|
-
for (const name of DEBUG_ENV_NAMES) {
|
|
577
|
-
assertSelfTest(!capturedEnv.has(name), `${name} should be absent from captured env by default`);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const optInPlan = buildLaunchPlan(
|
|
581
|
-
{ ...baseOptions, settingSources: "all", bridge: true, exposeBuiltinTools: true, eventDebug: true },
|
|
582
|
-
{ pi: fakePi, node: process.execPath, sealedPath: sealedHostilePath },
|
|
583
|
-
"/bin/sh",
|
|
584
|
-
);
|
|
585
|
-
const optIns = envMap(optInPlan.envAssignments);
|
|
586
|
-
assertSelfTest(optIns.get("PI_CURSOR_SETTING_SOURCES") === "all", "setting source opt-in must be reflected");
|
|
587
|
-
assertSelfTest(optIns.get("PI_CURSOR_PI_TOOL_BRIDGE") === "1", "bridge opt-in must be reflected");
|
|
588
|
-
assertSelfTest(optIns.get("PI_CURSOR_EXPOSE_BUILTIN_TOOLS") === "1", "built-in exposure opt-in must be reflected");
|
|
589
|
-
assertSelfTest(optIns.get("PI_CURSOR_SDK_EVENT_DEBUG") === "1", "event debug opt-in must be reflected");
|
|
590
|
-
assertSelfTest(optIns.get("PI_CURSOR_SDK_EVENT_DEBUG_DIR") === join(tempDir, "self-test.cursor-sdk-events"), "event debug dir must be deterministic under out-dir");
|
|
591
|
-
for (const name of DEBUG_ENV_NAMES) {
|
|
592
|
-
assertSelfTest(optInPlan.clearEnvNames.includes(name), `${name} must be cleared even when event debug is explicit`);
|
|
593
|
-
}
|
|
594
|
-
const eventDebugProbe = run("/bin/sh", ["-c", optInPlan.script], { env: hostileEnv });
|
|
595
|
-
assertSelfTest(eventDebugProbe.status === 0, `fake-pi event-debug env capture exited ${eventDebugProbe.status}: ${eventDebugProbe.stderr?.toString() ?? ""}`);
|
|
596
|
-
const capturedEventDebugEnv = parseEnvCapture(envCapture);
|
|
597
|
-
assertSelfTest(capturedEventDebugEnv.get("PI_CURSOR_SDK_EVENT_DEBUG") === "1", "event debug should be explicitly enabled");
|
|
598
|
-
assertSelfTest(capturedEventDebugEnv.get("PI_CURSOR_SDK_EVENT_DEBUG_DIR") === join(tempDir, "self-test.cursor-sdk-events"), "event debug dir should be deterministic under out-dir");
|
|
599
|
-
assertSelfTest(!capturedEventDebugEnv.has("PI_CURSOR_SDK_EVENT_DEBUG_RUN_DIR"), "stale event debug run dir should be cleared");
|
|
600
|
-
assertSelfTest(!capturedEventDebugEnv.has("PI_CURSOR_SDK_EVENT_DEBUG_SESSION_DIR"), "stale event debug session dir should be cleared");
|
|
601
|
-
assertSelfTest(!capturedEventDebugEnv.has("PI_CURSOR_SDK_EVENT_DEBUG_STDERR"), "stale event debug stderr flag should be cleared");
|
|
602
|
-
|
|
603
|
-
const fakeTmux = join(binDir, "tmux");
|
|
604
|
-
const deleteBufferMarker = join(tempDir, "delete-buffer-called");
|
|
605
|
-
writeFileSync(
|
|
606
|
-
fakeTmux,
|
|
607
|
-
`#!/bin/sh\ncase "$1" in\n -V) echo 'tmux fake'; exit 0 ;;\n new-session) exit 0 ;;\n load-buffer) cat >/dev/null; exit 0 ;;\n paste-buffer) exit 77 ;;\n delete-buffer) echo deleted > ${shellQuote(deleteBufferMarker)}; exit 0 ;;\n kill-session) exit 0 ;;\n *) echo "unexpected tmux command: $*" >&2; exit 64 ;;\nesac\n`,
|
|
608
|
-
"utf8",
|
|
609
|
-
);
|
|
610
|
-
chmodSync(fakeTmux, 0o755);
|
|
611
|
-
const originalPath = process.env.PATH;
|
|
612
|
-
try {
|
|
613
|
-
process.env.PATH = hostilePath;
|
|
614
|
-
let pasteFailed = false;
|
|
615
|
-
try {
|
|
616
|
-
runVisualSmoke({
|
|
617
|
-
...baseOptions,
|
|
618
|
-
prompt: "buffer cleanup prompt",
|
|
619
|
-
startupMs: 1,
|
|
620
|
-
waitMs: 1,
|
|
621
|
-
width: 80,
|
|
622
|
-
height: 24,
|
|
623
|
-
historyLines: 100,
|
|
624
|
-
});
|
|
625
|
-
} catch (error) {
|
|
626
|
-
pasteFailed = /paste-buffer failed/.test(error instanceof Error ? error.message : String(error));
|
|
627
|
-
}
|
|
628
|
-
assertSelfTest(pasteFailed, "fake tmux paste failure should exercise prompt-buffer cleanup path");
|
|
629
|
-
assertSelfTest(existsSync(deleteBufferMarker), "prompt tmux buffer should be deleted when paste/send fails");
|
|
630
|
-
} finally {
|
|
631
|
-
if (originalPath === undefined) delete process.env.PATH;
|
|
632
|
-
else process.env.PATH = originalPath;
|
|
633
|
-
}
|
|
634
|
-
console.log("[visual-smoke] self-test PASS");
|
|
635
|
-
} finally {
|
|
636
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
475
|
|
|
640
476
|
const options = parseArgs(process.argv.slice(2));
|
|
477
|
+
let artifacts;
|
|
641
478
|
try {
|
|
642
479
|
if (options.selfTest) {
|
|
643
|
-
|
|
480
|
+
runVisualSmokeSelfTest({
|
|
481
|
+
ROOT,
|
|
482
|
+
DEFAULT_MODE,
|
|
483
|
+
DEFAULT_MODEL,
|
|
484
|
+
DEFAULT_SETTING_SOURCES,
|
|
485
|
+
DEBUG_ENV_NAMES,
|
|
486
|
+
shellQuote,
|
|
487
|
+
parseArgs,
|
|
488
|
+
snapshotJsonlMtimes,
|
|
489
|
+
findLatestJsonl,
|
|
490
|
+
sealedNodePath,
|
|
491
|
+
resolveCommand,
|
|
492
|
+
requireNode,
|
|
493
|
+
requireCommand,
|
|
494
|
+
buildLaunchPlan,
|
|
495
|
+
run,
|
|
496
|
+
runVisualSmoke,
|
|
497
|
+
});
|
|
644
498
|
process.exit(0);
|
|
645
499
|
}
|
|
646
|
-
|
|
500
|
+
artifacts = runVisualSmoke(options);
|
|
501
|
+
writeVisualManifest(artifacts.manifestPath, options, artifacts);
|
|
647
502
|
checkLeftovers(options.leftoverPatterns);
|
|
648
503
|
if (options.screenshot) {
|
|
649
504
|
await writeTerminalScreenshot(artifacts.htmlPath, artifacts.pngPath, options.width, options.height);
|
|
505
|
+
artifacts.pngWritten = true;
|
|
506
|
+
writeVisualManifest(artifacts.manifestPath, options, artifacts);
|
|
650
507
|
}
|
|
651
508
|
console.log("[visual-smoke] artifacts:");
|
|
652
509
|
console.log(` ansi: ${artifacts.ansiPath}`);
|
|
@@ -655,6 +512,15 @@ try {
|
|
|
655
512
|
if (options.screenshot) console.log(` png: ${artifacts.pngPath}`);
|
|
656
513
|
console.log(` jsonl.path: ${artifacts.jsonlPathFile}`);
|
|
657
514
|
console.log(` jsonl: ${artifacts.jsonlPath}`);
|
|
515
|
+
console.log(` manifest: ${artifacts.manifestPath}`);
|
|
658
516
|
} catch (error) {
|
|
659
|
-
|
|
517
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
518
|
+
if (artifacts?.manifestPath) {
|
|
519
|
+
try {
|
|
520
|
+
writeVisualManifest(artifacts.manifestPath, options, artifacts, { message, writtenAt: new Date().toISOString() });
|
|
521
|
+
} catch {
|
|
522
|
+
// Preserve the original failure.
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
fail(message);
|
|
660
526
|
}
|
package/src/context.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import type { SDKImage } from "@cursor/sdk";
|
|
4
|
+
import type { AgentModeOption, SDKImage } from "@cursor/sdk";
|
|
5
5
|
import { getCursorReplayPromptLabel } from "./cursor-tool-presentation-registry.js";
|
|
6
6
|
|
|
7
7
|
export interface CursorPrompt {
|
|
@@ -13,6 +13,7 @@ export interface CursorPromptOptions {
|
|
|
13
13
|
maxInputTokens?: number;
|
|
14
14
|
charsPerToken?: number;
|
|
15
15
|
imageTokenEstimate?: number;
|
|
16
|
+
agentMode?: AgentModeOption;
|
|
16
17
|
/** Compact callable-surface summary; included on bootstrap prompts when set. */
|
|
17
18
|
toolManifest?: string;
|
|
18
19
|
}
|
|
@@ -21,35 +22,49 @@ export const CURSOR_APPROX_CHARS_PER_TOKEN = 4;
|
|
|
21
22
|
export const CURSOR_IMAGE_TOKEN_ESTIMATE = 1200;
|
|
22
23
|
const SECTION_SEPARATOR = "\n\n";
|
|
23
24
|
|
|
24
|
-
export function
|
|
25
|
+
export function getCursorPlanModeToolGuidanceText(agentMode: AgentModeOption | undefined): string | undefined {
|
|
26
|
+
if (agentMode !== "plan") return undefined;
|
|
27
|
+
return [
|
|
28
|
+
"Cursor SDK mode is plan for this run. In pi-cursor-sdk, plan mode may still use available Cursor SDK/MCP tools for inspection when needed.",
|
|
29
|
+
"Safe/read-only shell commands that inspect or print information are allowed when Cursor chooses to call Shell; do not say Shell is blocked by plan mode and then call it anyway.",
|
|
30
|
+
"Exposed pi__* bridge tools are also callable in plan mode when the user asks for them or they are needed to answer.",
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getCursorToolTailGuardText(
|
|
35
|
+
options: Pick<CursorPromptOptions, "agentMode"> & { includePlanModeGuidance?: boolean } = {},
|
|
36
|
+
): string {
|
|
25
37
|
return [
|
|
26
38
|
"Shell: use an explicit `cd` to the repo path when running project commands; session cwd may not match paths in tool args.",
|
|
39
|
+
options.includePlanModeGuidance === false ? undefined : getCursorPlanModeToolGuidanceText(options.agentMode),
|
|
40
|
+
"Exact-output requests: if the latest user asks to reply exactly, output exactly that text and do not add preambles, diagnostics, or repo checks unless explicitly requested.",
|
|
27
41
|
"Tool boundary reminder: If a tool is needed, call an available Cursor SDK/MCP tool. Never print a tool card (for example Tool call/Shell/command) as assistant text.",
|
|
28
|
-
].join("\n");
|
|
42
|
+
].filter((line): line is string => line !== undefined).join("\n");
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
function getCursorToolBoundaryText(options: { hasToolManifest?: boolean } = {}): string {
|
|
45
|
+
function getCursorToolBoundaryText(options: Pick<CursorPromptOptions, "agentMode"> & { hasToolManifest?: boolean } = {}): string {
|
|
32
46
|
const lines = [
|
|
33
47
|
"Cursor SDK tool boundary:",
|
|
34
48
|
"Call only tools exposed by Cursor SDK in this run. Pi tool names, replay labels, and transcript names are context only—not callable.",
|
|
35
49
|
"Bridged pi tools: call pi__* MCP names when exposed, not the pi card name in history. Replay activity is display-only.",
|
|
36
50
|
"Do not claim pi-side or WebSearch/WebFetch tools unless Cursor executes an equivalent tool.",
|
|
37
51
|
"Use pi__cursor_ask_question for material choices if exposed.",
|
|
52
|
+
getCursorPlanModeToolGuidanceText(options.agentMode),
|
|
38
53
|
"Images: only the latest user message's images are sent as bytes; ask to reattach or describe prior images.",
|
|
39
|
-
];
|
|
54
|
+
].filter((line): line is string => line !== undefined);
|
|
40
55
|
if (options.hasToolManifest) {
|
|
41
56
|
lines.push("See callable tool surfaces block below.");
|
|
42
57
|
}
|
|
43
58
|
return lines.join("\n");
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
function getCursorBootstrapTailSections(): string[] {
|
|
61
|
+
function getCursorBootstrapTailSections(options: Pick<CursorPromptOptions, "agentMode"> = {}): string[] {
|
|
47
62
|
return [
|
|
48
63
|
[
|
|
49
64
|
"Answer the latest user request above using Cursor SDK capabilities only. Do not list, promise, or call pi-only tools from the system prompt as if they were available.",
|
|
50
65
|
"If web research is requested, do not claim it unless a Cursor web/search/browser/MCP tool ran.",
|
|
51
66
|
].join("\n"),
|
|
52
|
-
getCursorToolTailGuardText(),
|
|
67
|
+
getCursorToolTailGuardText({ ...options, includePlanModeGuidance: false }),
|
|
53
68
|
];
|
|
54
69
|
}
|
|
55
70
|
|
|
@@ -366,7 +381,7 @@ export function buildCursorIncrementalPrompt(context: Context, options: CursorPr
|
|
|
366
381
|
const parts = applyPromptBudget(
|
|
367
382
|
sectionsBeforeMessages,
|
|
368
383
|
latestUserMessageSections,
|
|
369
|
-
[getCursorToolTailGuardText()],
|
|
384
|
+
[getCursorToolTailGuardText(options)],
|
|
370
385
|
latestUserMessageIndex,
|
|
371
386
|
budgetOptions,
|
|
372
387
|
);
|
|
@@ -374,7 +389,7 @@ export function buildCursorIncrementalPrompt(context: Context, options: CursorPr
|
|
|
374
389
|
}
|
|
375
390
|
|
|
376
391
|
export function buildCursorPrompt(context: Context, options: CursorPromptOptions = {}): CursorPrompt {
|
|
377
|
-
const sectionsBeforeMessages: string[] = [getCursorToolBoundaryText({ hasToolManifest: Boolean(options.toolManifest) })];
|
|
392
|
+
const sectionsBeforeMessages: string[] = [getCursorToolBoundaryText({ agentMode: options.agentMode, hasToolManifest: Boolean(options.toolManifest) })];
|
|
378
393
|
if (options.toolManifest) {
|
|
379
394
|
sectionsBeforeMessages.push(options.toolManifest);
|
|
380
395
|
}
|
|
@@ -390,7 +405,7 @@ export function buildCursorPrompt(context: Context, options: CursorPromptOptions
|
|
|
390
405
|
return text ? { index, text } : undefined;
|
|
391
406
|
})
|
|
392
407
|
.filter((section): section is { index: number; text: string } => section !== undefined);
|
|
393
|
-
const sectionsAfterMessages = getCursorBootstrapTailSections();
|
|
408
|
+
const sectionsAfterMessages = getCursorBootstrapTailSections(options);
|
|
394
409
|
const images = extractLatestImages(messages);
|
|
395
410
|
const imageTokenReserve = images.length * (options.imageTokenEstimate ?? 0);
|
|
396
411
|
const budgetOptions =
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { arePiToolsDisabled } from "./cursor-active-tools.js";
|
|
2
3
|
import {
|
|
3
4
|
CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES,
|
|
4
5
|
isNativeCursorToolName,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
NATIVE_CURSOR_TOOL_DISPLAY_ENV,
|
|
14
15
|
readBooleanEnv,
|
|
15
16
|
registeredNativeToolNames,
|
|
17
|
+
setCursorNativeToolDisplayRuntimeRequested,
|
|
16
18
|
skippedNativeToolNames,
|
|
17
19
|
} from "./cursor-native-tool-display-state.js";
|
|
18
20
|
import { isCursorReplayToolName } from "./cursor-tool-presentation-registry.js";
|
|
@@ -70,37 +72,41 @@ function hasAttemptedNativeCursorToolRegistration(): boolean {
|
|
|
70
72
|
return registeredNativeToolNames.size > 0 || skippedNativeToolNames.size > 0;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
function removeRegisteredNonCoreNativeCursorTools(pi: CursorNativeToolActivationApi): void {
|
|
76
|
+
if (registeredNativeToolNames.size === 0) return;
|
|
77
|
+
const activeToolNames = new Set(pi.getActiveTools());
|
|
78
|
+
let changed = false;
|
|
79
|
+
for (const toolName of registeredNativeToolNames) {
|
|
80
|
+
if (isCursorCorePiReplayToolName(toolName)) continue;
|
|
81
|
+
if (!activeToolNames.delete(toolName)) continue;
|
|
82
|
+
changed = true;
|
|
83
|
+
}
|
|
84
|
+
if (changed) pi.setActiveTools([...activeToolNames]);
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
export function syncRegisteredNativeCursorToolsForModel(
|
|
74
88
|
pi: CursorNativeToolActivationApi,
|
|
75
89
|
model: ExtensionContext["model"],
|
|
76
90
|
): void {
|
|
77
91
|
if (registeredNativeToolNames.size === 0) return;
|
|
92
|
+
if (!isCursorModel(model)) {
|
|
93
|
+
removeRegisteredNonCoreNativeCursorTools(pi);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (arePiToolsDisabled(pi)) return;
|
|
78
97
|
const activeToolNames = new Set(pi.getActiveTools());
|
|
79
98
|
let changed = false;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
changed = true;
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
for (const toolName of registeredNativeToolNames) {
|
|
89
|
-
if (isCursorCorePiReplayToolName(toolName)) continue;
|
|
90
|
-
if (!activeToolNames.delete(toolName)) continue;
|
|
91
|
-
changed = true;
|
|
92
|
-
}
|
|
99
|
+
for (const toolName of registeredNativeToolNames) {
|
|
100
|
+
if (isCursorReplayToolName(toolName) && !CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES.some((activeReplayToolName) => activeReplayToolName === toolName)) continue;
|
|
101
|
+
if (activeToolNames.has(toolName)) continue;
|
|
102
|
+
activeToolNames.add(toolName);
|
|
103
|
+
changed = true;
|
|
93
104
|
}
|
|
94
105
|
if (changed) pi.setActiveTools([...activeToolNames]);
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
async function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): Promise<void> {
|
|
98
|
-
if (!
|
|
99
|
-
registeredNativeToolNames.clear();
|
|
100
|
-
skippedNativeToolNames.clear();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (ctx.mode !== "tui" || !isCursorModel(ctx.model) || hasAttemptedNativeCursorToolRegistration()) return;
|
|
109
|
+
if (!isCursorModel(ctx.model) || hasAttemptedNativeCursorToolRegistration()) return;
|
|
104
110
|
|
|
105
111
|
const nonCoreToolNames = NATIVE_CURSOR_TOOL_NAMES.filter((toolName) => !isCursorCorePiReplayToolName(toolName));
|
|
106
112
|
const skippedToolNames = [
|
|
@@ -111,9 +117,13 @@ async function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolReg
|
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
async function ensureThenSyncNativeCursorToolsForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): Promise<void> {
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
const requested = isCursorNativeToolRegistrationRequested(ctx.mode);
|
|
121
|
+
setCursorNativeToolDisplayRuntimeRequested(requested);
|
|
122
|
+
if (!requested) {
|
|
123
|
+
removeRegisteredNonCoreNativeCursorTools(pi);
|
|
124
|
+
return;
|
|
116
125
|
}
|
|
126
|
+
await ensureNativeCursorToolsRegisteredForModel(pi, ctx);
|
|
117
127
|
syncRegisteredNativeCursorToolsForModel(pi, ctx.model);
|
|
118
128
|
}
|
|
119
129
|
|
|
@@ -13,18 +13,25 @@ export const registeredNativeToolNames = new Set<string>();
|
|
|
13
13
|
export const skippedNativeToolNames = new Set<string>();
|
|
14
14
|
export const nativeToolResults = new Map<string, CursorNativeToolDisplayItem>();
|
|
15
15
|
|
|
16
|
+
let nativeToolDisplayRuntimeRequested = false;
|
|
17
|
+
|
|
16
18
|
export function readBooleanEnv(name: string, env: Record<string, string | undefined> = process.env): boolean | undefined {
|
|
17
19
|
return parseOptionalEnvBoolean(env[name]);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
export function isCursorNativeToolDisplayRequested(): boolean {
|
|
22
|
+
export function isCursorNativeToolDisplayRequested(mode?: string): boolean {
|
|
21
23
|
const override = readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV);
|
|
22
24
|
if (override !== undefined) return override;
|
|
25
|
+
if (mode) return mode === "tui" || mode === "json" || mode === "rpc";
|
|
23
26
|
return process.stdout.isTTY === true;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
export function isCursorNativeToolRegistrationRequested(): boolean {
|
|
27
|
-
return readBooleanEnv(NATIVE_CURSOR_TOOL_REGISTRATION_ENV) !== false && isCursorNativeToolDisplayRequested();
|
|
29
|
+
export function isCursorNativeToolRegistrationRequested(mode?: string): boolean {
|
|
30
|
+
return mode !== "print" && readBooleanEnv(NATIVE_CURSOR_TOOL_REGISTRATION_ENV) !== false && isCursorNativeToolDisplayRequested(mode);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setCursorNativeToolDisplayRuntimeRequested(requested: boolean): void {
|
|
34
|
+
nativeToolDisplayRuntimeRequested = requested;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export function isCursorNativeToolDisplayEnabled(): boolean {
|
|
@@ -32,7 +39,7 @@ export function isCursorNativeToolDisplayEnabled(): boolean {
|
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
export function isCursorNativeToolDisplayRuntimeEnabled(): boolean {
|
|
35
|
-
return
|
|
42
|
+
return nativeToolDisplayRuntimeRequested && readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) !== false && registeredNativeToolNames.size > 0;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
export function canRenderCursorToolNatively(toolName: string): boolean {
|
|
@@ -70,9 +77,11 @@ export function isCursorFileMutationToolName(toolName: string): toolName is "edi
|
|
|
70
77
|
export const __testUtils = {
|
|
71
78
|
nativeToolResultCount: () => nativeToolResults.size,
|
|
72
79
|
registerNativeToolNameForTests(toolName: string): void {
|
|
80
|
+
nativeToolDisplayRuntimeRequested = true;
|
|
73
81
|
registeredNativeToolNames.add(toolName);
|
|
74
82
|
},
|
|
75
83
|
reset(): void {
|
|
84
|
+
nativeToolDisplayRuntimeRequested = false;
|
|
76
85
|
registeredNativeToolNames.clear();
|
|
77
86
|
skippedNativeToolNames.clear();
|
|
78
87
|
nativeToolResults.clear();
|
|
@@ -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) {
|