Merge PR #8183: fix(cursor): omit undefined exec args and keep exec-resolved under owned dialects (@jairuspace)
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Cursor sessions re-executing settled tools when an owned dialect projector rebuilds toolCall blocks: `snapshotAssistantContentBlock` now copies `kCursorExecResolved` explicitly so agent-loop still skips already-settled calls.
|
||||
|
||||
## [17.2.10] - 2026-08-06
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -31,7 +31,11 @@ import {
|
||||
wrapInbandToolStream,
|
||||
} from "@oh-my-pi/pi-ai/dialect";
|
||||
import * as AIError from "@oh-my-pi/pi-ai/error";
|
||||
import { type CursorExecResolvedCarrier, kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
||||
import {
|
||||
type CursorExecResolvedCarrier,
|
||||
copyCursorExecResolved,
|
||||
kCursorExecResolved,
|
||||
} from "@oh-my-pi/pi-ai/utils/block-symbols";
|
||||
import {
|
||||
createHarmonyAuditEvent,
|
||||
detectHarmonyLeakInAssistantMessage,
|
||||
@@ -356,12 +360,18 @@ function snapshotAssistantContentBlock(block: AssistantContentBlock): AssistantC
|
||||
return { ...block, block: structuredCloneJSON(block.block) };
|
||||
case "fallback":
|
||||
return { ...block, from: { ...block.from }, to: { ...block.to } };
|
||||
case "toolCall":
|
||||
return {
|
||||
case "toolCall": {
|
||||
const snap = {
|
||||
...block,
|
||||
arguments: structuredCloneJSON(block.arguments),
|
||||
providerMetadata: snapshotToolCallProviderMetadata(block.providerMetadata),
|
||||
};
|
||||
// Object spread copies enumerable symbols in Bun, but the Cursor
|
||||
// exec-resolved marker is load-bearing for skip-on-dispatch — copy
|
||||
// it explicitly so a projector/snapshot path cannot drop it.
|
||||
copyCursorExecResolved(snap, block);
|
||||
return snap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
- Fixed the Amazon Bedrock and Cursor transports ignoring `StreamOptions.headers`; both built their request headers from scratch, so caller-supplied tracing or attribution headers were silently dropped while working on every other provider ([#8107](https://github.com/can1357/oh-my-pi/pull/8107) by [@svperfecta](https://github.com/svperfecta)).
|
||||
- Fixed Antigravity Flash turns hanging after successful response headers when the endpoint never emitted an SSE event; the provider now cancels the stalled body and fails over after 60 seconds while retaining the longer allowance for Pro reasoning starts.
|
||||
- Fixed Cursor exec-bridge bash/grep calls failing ArkType validation when the server omitted optional frame fields: synthesized and executed tool args now drop `undefined` keys (`cwd`, `case`, `skip`, `timeout`) instead of writing `optional: value || undefined`.
|
||||
- Fixed Cursor sessions double-executing settled tools when `tools.format` is an owned dialect (e.g. `gemini`): `wrapInbandToolStream` rebuilt toolCall blocks without copying `kCursorExecResolved`, so agent-loop re-ran bash/grep/todo and appended a second result for the same call id.
|
||||
|
||||
## [17.2.12] - 2026-08-08
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
} from "../types";
|
||||
import {
|
||||
clearStreamingPartialJson,
|
||||
copyCursorExecResolved,
|
||||
getStreamingPartialJson,
|
||||
type StreamingPartialJsonCarrier,
|
||||
setStreamingPartialJson,
|
||||
@@ -54,6 +55,7 @@ function cloneToolCall(source: StreamingToolCall): StreamingToolCall {
|
||||
};
|
||||
const partialJson = getStreamingPartialJson(source);
|
||||
if (partialJson !== undefined) setStreamingPartialJson(block, partialJson);
|
||||
copyCursorExecResolved(block, source);
|
||||
return block;
|
||||
}
|
||||
|
||||
@@ -65,6 +67,7 @@ function syncToolCall(target: StreamingToolCall, source: StreamingToolCall): voi
|
||||
const partialJson = getStreamingPartialJson(source);
|
||||
if (partialJson === undefined) clearStreamingPartialJson(target);
|
||||
else setStreamingPartialJson(target, partialJson);
|
||||
copyCursorExecResolved(target, source);
|
||||
}
|
||||
|
||||
function hasNamedNativeToolCall(source: StreamingToolCall | undefined): source is StreamingToolCall {
|
||||
|
||||
@@ -163,3 +163,25 @@ export function piLimit(limit: number | undefined): number | undefined {
|
||||
export function piTimeout(timeout: number | undefined): number | undefined {
|
||||
return timeout !== undefined && timeout >= 0 ? timeout : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop keys whose value is `undefined` so optional local-tool kwargs stay
|
||||
* absent rather than present-as-undefined.
|
||||
*
|
||||
* The Cursor exec bridge historically wrote forms like
|
||||
* `cwd: workingDirectory || undefined` and
|
||||
* `case: caseInsensitive === true ? false : undefined`. ArkType rejects a
|
||||
* present `undefined` on an optional field (`was undefined`) even though
|
||||
* omitting the key is valid — which flooded Cursor sessions with bash/grep
|
||||
* validation errors for otherwise fine frames.
|
||||
*/
|
||||
export function omitUndefinedArgs<T extends Record<string, unknown>>(
|
||||
args: T,
|
||||
): { [K in keyof T]?: Exclude<T[K], undefined> } {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const key of Object.keys(args)) {
|
||||
const value = args[key];
|
||||
if (value !== undefined) out[key] = value;
|
||||
}
|
||||
return out as { [K in keyof T]?: Exclude<T[K], undefined> };
|
||||
}
|
||||
|
||||
@@ -210,6 +210,7 @@ import {
|
||||
buildPiWriteError,
|
||||
buildPiWriteRejected,
|
||||
buildPiWriteResult,
|
||||
omitUndefinedArgs,
|
||||
piEscapeRegexLiteral,
|
||||
piGrepSkip,
|
||||
piJoinPath,
|
||||
@@ -3668,11 +3669,14 @@ export function synthesizeCursorExecToolCall(
|
||||
): void {
|
||||
endCurrentTextBlock(output, stream, state);
|
||||
endCurrentThinkingBlock(output, stream, state);
|
||||
// Exec-frame translators often write `optional: value || undefined`. A
|
||||
// present `undefined` fails ArkType optional-field validation; drop those
|
||||
// keys so the transcript block matches what a model-native call would omit.
|
||||
const block: ToolCallState = {
|
||||
type: "toolCall",
|
||||
id: toolCallId,
|
||||
name: toolName,
|
||||
arguments: args,
|
||||
arguments: omitUndefinedArgs(args),
|
||||
[kStreamingBlockIndex]: output.content.length,
|
||||
[kStreamingBlockKind]: "cursor-exec",
|
||||
[kCursorExecResolved]: true,
|
||||
|
||||
@@ -74,6 +74,7 @@ import type { ToolResultMessage } from "../../types";
|
||||
* and their translation are consumed together.
|
||||
*/
|
||||
export {
|
||||
omitUndefinedArgs,
|
||||
piEscapeRegexLiteral,
|
||||
piGrepSkip,
|
||||
piJoinPath,
|
||||
|
||||
@@ -59,6 +59,24 @@ export const kCursorExecResolved = Symbol("provider.block.cursorExecResolved");
|
||||
/** Carries the resolved marker without exposing a string-keyed property. */
|
||||
export type CursorExecResolvedCarrier = object & { [kCursorExecResolved]?: true };
|
||||
|
||||
/** True when a toolCall block was already executed by Cursor's exec channel. */
|
||||
export function isCursorExecResolved(block: CursorExecResolvedCarrier | null | undefined): boolean {
|
||||
return block?.[kCursorExecResolved] === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy {@link kCursorExecResolved} onto a cloned/projected toolCall block.
|
||||
*
|
||||
* Stream projectors (owned/in-band dialect, leaked-thinking heal) rebuild
|
||||
* toolCall objects field-by-field. Dropping this marker lets `agent-loop.ts`
|
||||
* re-execute a call Cursor already settled — duplicate toolResults and a
|
||||
* second bash/write/delete. Partial-JSON is already copied explicitly; this
|
||||
* marker is the other load-bearing symbol that must survive the same way.
|
||||
*/
|
||||
export function copyCursorExecResolved(target: CursorExecResolvedCarrier, source: CursorExecResolvedCarrier): void {
|
||||
if (source[kCursorExecResolved] === true) target[kCursorExecResolved] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a text block synthesized by cross-model thinking demotion in
|
||||
* `transformMessages`. Converters that flatten adjacent text blocks into one
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
} from "../types";
|
||||
import {
|
||||
clearStreamingPartialJson,
|
||||
copyCursorExecResolved,
|
||||
getStreamingPartialJson,
|
||||
type StreamingPartialJsonCarrier,
|
||||
setStreamingPartialJson,
|
||||
@@ -49,6 +50,7 @@ function cloneToolCall(source: StreamingToolCall): StreamingToolCall {
|
||||
const block: StreamingToolCall = { ...source, arguments: source.arguments };
|
||||
const partialJson = getStreamingPartialJson(source);
|
||||
if (partialJson !== undefined) setStreamingPartialJson(block, partialJson);
|
||||
copyCursorExecResolved(block, source);
|
||||
return block;
|
||||
}
|
||||
|
||||
@@ -57,6 +59,7 @@ function syncToolCall(target: StreamingToolCall, source: StreamingToolCall): voi
|
||||
const partialJson = getStreamingPartialJson(source);
|
||||
if (partialJson === undefined) clearStreamingPartialJson(target);
|
||||
else setStreamingPartialJson(target, partialJson);
|
||||
copyCursorExecResolved(target, source);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { type } from "@oh-my-pi/omptype";
|
||||
import { omitUndefinedArgs, piGrepSkip } from "../src/providers/cursor-pi-args";
|
||||
import type { Tool } from "../src/types";
|
||||
import { validateToolArguments } from "../src/utils/validation";
|
||||
|
||||
describe("omitUndefinedArgs", () => {
|
||||
it("drops keys whose value is undefined and keeps defined optionals", () => {
|
||||
expect(
|
||||
omitUndefinedArgs({
|
||||
command: "pwd",
|
||||
cwd: undefined,
|
||||
timeout: 30,
|
||||
}),
|
||||
).toEqual({ command: "pwd", timeout: 30 });
|
||||
expect(
|
||||
omitUndefinedArgs({
|
||||
pattern: "needle",
|
||||
path: ".",
|
||||
case: false,
|
||||
skip: piGrepSkip(undefined),
|
||||
}),
|
||||
).toEqual({ pattern: "needle", path: ".", case: false });
|
||||
});
|
||||
|
||||
it("makes Cursor-style bash/grep frames pass ArkType optional-field validation", () => {
|
||||
const bashTool: Tool = {
|
||||
name: "bash",
|
||||
description: "",
|
||||
parameters: type({
|
||||
command: type("string").describe("command to execute"),
|
||||
"timeout?": type("number").describe("timeout"),
|
||||
"cwd?": type("string").describe("working directory"),
|
||||
}),
|
||||
};
|
||||
const grepTool: Tool = {
|
||||
name: "grep",
|
||||
description: "",
|
||||
parameters: type({
|
||||
pattern: type("string").describe("regex pattern"),
|
||||
"path?": type("string").describe("path"),
|
||||
"case?": type("boolean").describe("case-sensitive search"),
|
||||
"skip?": type("number").or("null").describe("files to skip"),
|
||||
}),
|
||||
};
|
||||
|
||||
// Mirrors the Cursor bridge: empty workingDirectory → `cwd: undefined`.
|
||||
const workingDirectory = "";
|
||||
const rawBash = {
|
||||
command: "git status",
|
||||
cwd: workingDirectory || undefined,
|
||||
timeout: 30,
|
||||
};
|
||||
expect(() =>
|
||||
validateToolArguments(bashTool, { type: "toolCall", id: "b1", name: "bash", arguments: rawBash }),
|
||||
).toThrow(/cwd must be working directory \(was undefined\)/);
|
||||
expect(
|
||||
validateToolArguments(bashTool, {
|
||||
type: "toolCall",
|
||||
id: "b2",
|
||||
name: "bash",
|
||||
arguments: omitUndefinedArgs(rawBash),
|
||||
}),
|
||||
).toEqual({ command: "git status", timeout: 30 });
|
||||
|
||||
// Mirrors the Cursor bridge: caseInsensitive unset → `case: undefined`.
|
||||
const caseInsensitive: boolean | undefined = undefined;
|
||||
const rawGrep = {
|
||||
pattern: "needle",
|
||||
path: ".",
|
||||
case: caseInsensitive === true ? false : undefined,
|
||||
skip: piGrepSkip(undefined),
|
||||
};
|
||||
expect(() =>
|
||||
validateToolArguments(grepTool, { type: "toolCall", id: "g1", name: "grep", arguments: rawGrep }),
|
||||
).toThrow(/case must be case-sensitive search \(was undefined\)/);
|
||||
expect(
|
||||
validateToolArguments(grepTool, {
|
||||
type: "toolCall",
|
||||
id: "g2",
|
||||
name: "grep",
|
||||
arguments: omitUndefinedArgs(rawGrep),
|
||||
}),
|
||||
).toEqual({ pattern: "needle", path: "." });
|
||||
});
|
||||
});
|
||||
@@ -412,11 +412,44 @@ describe("synthesizeCursorExecToolCall (issue #4348)", () => {
|
||||
type: "toolCall",
|
||||
id: "t2",
|
||||
name: "bash",
|
||||
arguments: { command: "echo hi", cwd: undefined, timeout: undefined },
|
||||
// Undefined optional kwargs are dropped so ArkType optional-field
|
||||
// validation does not reject the synthesized block.
|
||||
arguments: { command: "echo hi" },
|
||||
});
|
||||
expect(t3).toMatchObject({ type: "text", text: "done" });
|
||||
});
|
||||
|
||||
it("omits undefined optional kwargs from synthesized exec tool args", () => {
|
||||
const h = newHarness();
|
||||
synthesizeCursorExecToolCall(h.output, h.stream, h.state, "bash-1", "bash", {
|
||||
command: "pwd",
|
||||
cwd: undefined,
|
||||
timeout: 30,
|
||||
});
|
||||
synthesizeCursorExecToolCall(h.output, h.stream, h.state, "grep-1", "grep", {
|
||||
pattern: "needle",
|
||||
path: ".",
|
||||
case: undefined,
|
||||
skip: undefined,
|
||||
});
|
||||
const [bashCall, grepCall] = h.output.content;
|
||||
expect(bashCall).toMatchObject({
|
||||
type: "toolCall",
|
||||
id: "bash-1",
|
||||
name: "bash",
|
||||
arguments: { command: "pwd", timeout: 30 },
|
||||
});
|
||||
expect(Object.hasOwn((bashCall as { arguments: object }).arguments, "cwd")).toBe(false);
|
||||
expect(grepCall).toMatchObject({
|
||||
type: "toolCall",
|
||||
id: "grep-1",
|
||||
name: "grep",
|
||||
arguments: { pattern: "needle", path: "." },
|
||||
});
|
||||
expect(Object.hasOwn((grepCall as { arguments: object }).arguments, "case")).toBe(false);
|
||||
expect(Object.hasOwn((grepCall as { arguments: object }).arguments, "skip")).toBe(false);
|
||||
});
|
||||
|
||||
it("emits toolcall events at the exact index the block occupies in content", () => {
|
||||
const h = newHarness();
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { wrapInbandToolStream } from "../src/dialect/owned-stream";
|
||||
import type { AssistantMessage, AssistantMessageEvent, ThinkingContent, ToolCall, Usage } from "../src/types";
|
||||
import { getStreamingPartialJson, setStreamingPartialJson } from "../src/utils/block-symbols";
|
||||
import {
|
||||
getStreamingPartialJson,
|
||||
isCursorExecResolved,
|
||||
kCursorExecResolved,
|
||||
setStreamingPartialJson,
|
||||
} from "../src/utils/block-symbols";
|
||||
import { AssistantMessageEventStream } from "../src/utils/event-stream";
|
||||
|
||||
const TOOLS = [
|
||||
@@ -299,6 +304,29 @@ describe("wrapInbandToolStream native tool-call passthrough", () => {
|
||||
expect(events).toContain("toolcall_end");
|
||||
});
|
||||
|
||||
it("preserves kCursorExecResolved across the owned/in-band projector", async () => {
|
||||
// Cursor + tools.format: gemini wraps every provider stream in
|
||||
// wrapInbandToolStream. The projector rebuilds toolCall objects
|
||||
// field-by-field; dropping the exec-resolved marker lets agent-loop
|
||||
// re-run a call Cursor already settled.
|
||||
const inner = drive((push, out) => {
|
||||
const block: ToolCall = {
|
||||
type: "toolCall",
|
||||
id: "cursor-bash-1",
|
||||
name: "bash",
|
||||
arguments: { command: "echo hi" },
|
||||
};
|
||||
(block as ToolCall & { [kCursorExecResolved]?: true })[kCursorExecResolved] = true;
|
||||
out.content.push(block);
|
||||
push({ type: "toolcall_start", contentIndex: 0, partial: out });
|
||||
push({ type: "toolcall_end", contentIndex: 0, toolCall: block, partial: out });
|
||||
});
|
||||
const { message } = await collect(wrapInbandToolStream(inner, TOOLS, "gemini"));
|
||||
const calls = message.content.filter((b): b is ToolCall => b.type === "toolCall");
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(isCursorExecResolved(calls[0])).toBe(true);
|
||||
});
|
||||
|
||||
it("drops a nameless native ghost but keeps the real native call", async () => {
|
||||
const { message } = await collect(wrapInbandToolStream(ghostThenRealNative(), TOOLS, "gemini"));
|
||||
const calls = message.content.filter((b): b is ToolCall => b.type === "toolCall");
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
- Fixed timed-out LSP requests continuing to consume server CPU and block queued requests by sending `$/cancelRequest` ([#8116](https://github.com/can1357/oh-my-pi/issues/8116)).
|
||||
- Fixed `shutdownAll()` leaving the configured LSP idle checker alive and preventing short-lived SDK hosts from exiting ([#8115](https://github.com/can1357/oh-my-pi/issues/8115)).
|
||||
- Fixed terminal Mermaid borders and junctions using low-contrast UI chrome colors instead of the active theme's readable content color.
|
||||
- Fixed Cursor provider sessions flooding bash/grep validation errors (`cwd`/`case`/`skip` "was undefined") when Cursor omitted optional exec-frame fields; the exec bridge now omits unset optional kwargs before tool execution and transcript synthesis.
|
||||
|
||||
## [17.2.12] - 2026-08-08
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
ToolResultMessage,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import {
|
||||
omitUndefinedArgs,
|
||||
piEscapeRegexLiteral,
|
||||
piGrepSkip,
|
||||
piJoinPath,
|
||||
@@ -239,7 +240,11 @@ async function executeTool(
|
||||
return createToolResultMessage(toolCallId, toolName, result, true);
|
||||
}
|
||||
|
||||
options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args });
|
||||
// Same rule as synthesizeCursorExecToolCall: optional kwargs must be absent,
|
||||
// not `undefined`, or ArkType validation rejects the call.
|
||||
const toolArgs = omitUndefinedArgs(args);
|
||||
|
||||
options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: toolArgs });
|
||||
|
||||
let result: AgentToolResult<unknown>;
|
||||
let isError = false;
|
||||
@@ -254,7 +259,7 @@ async function executeTool(
|
||||
type: "tool_execution_update",
|
||||
toolCallId,
|
||||
toolName,
|
||||
args,
|
||||
args: toolArgs,
|
||||
partialResult: sanitizedResult,
|
||||
});
|
||||
}
|
||||
@@ -263,7 +268,7 @@ async function executeTool(
|
||||
try {
|
||||
result = await tool.execute(
|
||||
toolCallId,
|
||||
args as Record<string, unknown>,
|
||||
toolArgs as Record<string, unknown>,
|
||||
undefined,
|
||||
onUpdate,
|
||||
options.getToolContext?.(),
|
||||
@@ -509,11 +514,11 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
||||
}
|
||||
|
||||
const timeoutSeconds = args.timeout && args.timeout > 0 ? args.timeout : undefined;
|
||||
const toolArgs: Record<string, unknown> = {
|
||||
const toolArgs = omitUndefinedArgs({
|
||||
command: args.command,
|
||||
cwd: args.workingDirectory || undefined,
|
||||
timeout: timeoutSeconds,
|
||||
};
|
||||
});
|
||||
|
||||
this.options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: toolArgs });
|
||||
|
||||
|
||||
@@ -699,6 +699,67 @@ describe("CursorExecHandlers error results", () => {
|
||||
const end = events.find(event => event.type === "tool_execution_end");
|
||||
expect(end?.isError).toBe(true);
|
||||
});
|
||||
|
||||
it("omits unset optional kwargs from shellStream start events and execute args", async () => {
|
||||
// shellStream bypasses executeTool(), so omitUndefinedArgs must be
|
||||
// applied here directly — otherwise absent cwd/timeout become
|
||||
// present-undefined and ArkType rejects the bash call.
|
||||
const events: AgentEvent[] = [];
|
||||
const executeArgs: Record<string, unknown>[] = [];
|
||||
const bashTool: AgentTool = {
|
||||
name: "bash",
|
||||
label: "bash",
|
||||
description: "records args",
|
||||
parameters: type({ command: "string", "cwd?": "string", "timeout?": "number" }),
|
||||
execute: async (_id, args) => {
|
||||
executeArgs.push({ ...args });
|
||||
return { content: [{ type: "text", text: "ok" }], details: {} };
|
||||
},
|
||||
};
|
||||
const handlers = new CursorExecHandlers({
|
||||
cwd: ".",
|
||||
tools: new Map([["bash", bashTool]]),
|
||||
emitEvent: event => events.push(event),
|
||||
});
|
||||
|
||||
await handlers.shellStream(
|
||||
create(ShellArgsSchema, {
|
||||
toolCallId: "call-shell-omit",
|
||||
command: "echo hi",
|
||||
// Proto string defaults to ""; the bridge maps that to undefined.
|
||||
workingDirectory: "",
|
||||
}),
|
||||
{ onStdout: () => {}, onStderr: () => {} },
|
||||
);
|
||||
|
||||
const start = events.find(event => event.type === "tool_execution_start");
|
||||
expect(start?.type).toBe("tool_execution_start");
|
||||
if (start?.type !== "tool_execution_start") throw new Error("expected tool_execution_start");
|
||||
expect(start.args).toEqual({ command: "echo hi" });
|
||||
expect(Object.hasOwn(start.args, "cwd")).toBe(false);
|
||||
expect(Object.hasOwn(start.args, "timeout")).toBe(false);
|
||||
expect(executeArgs).toHaveLength(1);
|
||||
expect(executeArgs[0]).toEqual({ command: "echo hi" });
|
||||
expect(Object.hasOwn(executeArgs[0]!, "cwd")).toBe(false);
|
||||
expect(Object.hasOwn(executeArgs[0]!, "timeout")).toBe(false);
|
||||
|
||||
executeArgs.length = 0;
|
||||
events.length = 0;
|
||||
await handlers.shellStream(
|
||||
create(ShellArgsSchema, {
|
||||
toolCallId: "call-shell-keep",
|
||||
command: "pwd",
|
||||
workingDirectory: "/tmp",
|
||||
timeout: 12,
|
||||
}),
|
||||
{ onStdout: () => {}, onStderr: () => {} },
|
||||
);
|
||||
const keepStart = events.find(event => event.type === "tool_execution_start");
|
||||
expect(keepStart?.type).toBe("tool_execution_start");
|
||||
if (keepStart?.type !== "tool_execution_start") throw new Error("expected tool_execution_start");
|
||||
expect(keepStart.args).toEqual({ command: "pwd", cwd: "/tmp", timeout: 12 });
|
||||
expect(executeArgs[0]).toEqual({ command: "pwd", cwd: "/tmp", timeout: 12 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("CursorExecHandlers mounted tool bridge", () => {
|
||||
@@ -1537,9 +1598,9 @@ describe("CursorExecHandlers Pi frame translation", () => {
|
||||
|
||||
expect(calls).toEqual([
|
||||
{ pattern: "x", path: ".", case: false },
|
||||
// Case-sensitive is the local default, so `false` maps to "unset",
|
||||
// not to `case: true`.
|
||||
{ pattern: "x", path: ".", case: undefined },
|
||||
// Case-sensitive is the local default, so `false` maps to unset —
|
||||
// the key is omitted rather than written as `case: undefined`.
|
||||
{ pattern: "x", path: "." },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ function cursorTurn(): AgentMessage[] {
|
||||
type: "toolCall",
|
||||
id: "tc-bash",
|
||||
name: "bash",
|
||||
arguments: { command: "ls -1", cwd: undefined, timeout: undefined },
|
||||
arguments: { command: "ls -1" },
|
||||
},
|
||||
],
|
||||
api: "cursor-agent",
|
||||
|
||||
Reference in New Issue
Block a user