Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2155,7 +2155,7 @@ describe("adeRpcServer", () => {
expect(fixture.runtime.ptyService.writeBySessionId).not.toHaveBeenCalled();
});

it("passes Cursor initial input as the documented prompt argument", async () => {
it("submits Cursor initial input after the interactive CLI is ready", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });

Expand All @@ -2169,13 +2169,15 @@ describe("adeRpcServer", () => {
expect(response?.isError).toBeUndefined();
const createCall = fixture.runtime.ptyService.create.mock.calls.at(-1)?.[0];
expect(createCall?.command).toBe("cursor-agent");
expect(createCall?.args?.at(-1)).toContain("fix failing tests");
expect(createCall?.args).toEqual(expect.arrayContaining(["--model", "auto"]));
expect(createCall?.args).not.toContain(expect.stringContaining("fix failing tests"));
expect(createCall?.startupCommand).toContain("cursor-agent");
expect(createCall?.startupCommand).toContain("fix failing tests");
expect(createCall?.startupCommand).not.toContain("fix failing tests");
expect(createCall?.startupCommand).not.toContain("create-chat");
expect(createCall?.startupCommand).not.toContain("--resume");
expect(createCall?.initialInput).toBeUndefined();
expect(createCall?.initialInputDelayMs).toBeUndefined();
expect(createCall?.initialInput).toContain("ADE session guidance");
expect(createCall?.initialInput).toContain("fix failing tests");
expect(createCall?.initialInputDelayMs).toBe(750);
expect(fixture.runtime.ptyService.writeBySessionId).not.toHaveBeenCalled();
expect(fixture.runtime.ptyService.dispose).not.toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2453,7 +2453,7 @@ describe("createSyncRemoteCommandService", () => {
}))).rejects.toThrow("work.sendToSession requires text.");
});

it("work.startCliSession passes Cursor initial input as the documented prompt argument", async () => {
it("work.startCliSession submits Cursor initial input after the interactive CLI is ready", async () => {
await service.execute(makePayload("work.startCliSession", {
laneId: "lane-1",
provider: "cursor",
Expand All @@ -2462,14 +2462,16 @@ describe("createSyncRemoteCommandService", () => {

const call = ptyService.create.mock.calls.at(-1)?.[0];
expect(call?.startupCommand).toContain("cursor-agent");
expect(call?.startupCommand).toContain("fix the tests");
expect(call?.startupCommand).not.toContain("fix the tests");
expect(call?.startupCommand).not.toContain("cursor-agent create-chat");
expect(call?.startupCommand).not.toContain("--resume");
expect(call?.initialInput).toBeUndefined();
expect(call?.initialInputDelayMs).toBeUndefined();
expect(call?.initialInput).toContain("ADE session guidance");
expect(call?.initialInput).toContain("fix the tests");
expect(call?.initialInputDelayMs).toBe(750);
expect(call).not.toHaveProperty("awaitInitialInput");
expect(call?.command).toBe("cursor-agent");
expect(call?.args?.at(-1)).toContain("fix the tests");
expect(call?.args).toEqual(expect.arrayContaining(["--model", "auto"]));
expect(call?.args).not.toContain(expect.stringContaining("fix the tests"));
expect(ptyService.writeBySessionId).not.toHaveBeenCalled();
expect(ptyService.dispose).not.toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4933,7 +4933,10 @@ describe("AgentChatPane submit recovery", () => {
});
const launchArgs = onLaunchCliSession.mock.calls[0]?.[0];
expect(launchArgs.startupCommand).toContain(`--model ${fastAlias}`);
expect(launchArgs.args).toEqual(expect.arrayContaining([expect.stringContaining("Run Cursor in fast mode.")]));
expect(launchArgs.startupCommand).not.toContain("Run Cursor in fast mode.");
expect(launchArgs.args).not.toContain(expect.stringContaining("Run Cursor in fast mode."));
expect(launchArgs.initialInput).toContain("Run Cursor in fast mode.");
expect(launchArgs.initialInputDelayMs).toBe(750);
});

it("uses the OpenCode fast variant when launching a fast Work draft CLI session", async () => {
Expand Down Expand Up @@ -5125,7 +5128,10 @@ describe("AgentChatPane submit recovery", () => {
});
const launchArgs = onLaunchCliSession.mock.calls[0]?.[0];
expect(launchArgs.startupCommand).toContain(`--model ${concreteModel}`);
expect(launchArgs.args).toEqual(expect.arrayContaining([expect.stringContaining("Run Cursor with medium fast thinking.")]));
expect(launchArgs.startupCommand).not.toContain("Run Cursor with medium fast thinking.");
expect(launchArgs.args).not.toContain(expect.stringContaining("Run Cursor with medium fast thinking."));
expect(launchArgs.initialInput).toContain("Run Cursor with medium fast thinking.");
expect(launchArgs.initialInputDelayMs).toBe(750);
});

it("auto-creates a lane for a foreground CLI session draft", async () => {
Expand Down
32 changes: 20 additions & 12 deletions apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,20 +408,28 @@ describe("buildTrackedCliStartupCommand", () => {
});

describe("additional CLI providers", () => {
it("launches Cursor with the documented initial prompt argument", () => {
it("launches Cursor with initial prompts submitted through PTY input", () => {
const launch = buildTrackedCliLaunchCommand({ provider: "cursor", permissionMode: "plan", model: "cursor-fast", initialPrompt: "Review this lane." });
expect(launch.command).toBe("cursor-agent");
expect(launch.args).toEqual(expect.arrayContaining(["--mode", "plan", "--model", "cursor-fast"]));
expect(launch.args.at(-1)).toContain("ADE session guidance");
expect(launch.args.at(-1)).toContain("Review this lane.");
expect(launch.args).toEqual(["--mode", "plan", "--model", "cursor-fast"]);
expect(launch.startupCommand).toContain("cursor-agent --mode plan --model cursor-fast");
expect(launch.startupCommand).toContain("Review this lane.");
expect(launch.startupCommand).toContain("ADE session guidance");
expect(launch.startupCommand).not.toContain("Review this lane.");
expect(launch.startupCommand).not.toContain("ADE session guidance");
expect(launch.startupCommand).not.toContain("cursor-agent create-chat");
expect(launch.startupCommand).not.toContain("--resume");
expect(launch.initialInput).toContain("ADE session guidance");
expect(launch.initialInput).toContain("Review this lane.");
expect(launch.initialInputDelayMs).toBe(750);
expect(launch.env?.[ADE_AGENT_SKILLS_DIRS_ENV]).toContain("agent-skills");
});

it("keeps empty Cursor launches idle instead of submitting ADE guidance as work", () => {
const launch = buildTrackedCliLaunchCommand({ provider: "cursor", permissionMode: "default", model: "cursor-fast" });
expect(launch.command).toBe("cursor-agent");
expect(launch.args).toEqual(["--model", "cursor-fast"]);
expect(launch.startupCommand).toBe("cursor-agent --model cursor-fast");
expect(launch.initialInput).toBeUndefined();
expect(launch.initialInputDelayMs).toBeUndefined();
expect(launch.env?.[ADE_AGENT_SKILLS_DIRS_ENV]).toContain("agent-skills");
});

it("normalizes Cursor registry model ids and forces full-auto interactive workspaces", () => {
Expand All @@ -437,17 +445,17 @@ describe("buildTrackedCliStartupCommand", () => {
expect(launch.startupCommand).not.toContain("--model cursor/composer-2.5");
expect(launch.args).toEqual(expect.arrayContaining(["--force", "--model", "composer-2.5"]));
expect(launch.args).not.toContain("--trust");
expect(launch.args.at(-1)).toContain("Review this lane.");
expect(launch.args).not.toContain(expect.stringContaining("Review this lane."));
expect(launch.initialInput).toContain("Review this lane.");
});

it("keeps Cursor launch direct on Windows", () => {
withProcessPlatform("win32", () => {
const launch = buildTrackedCliLaunchCommand({ provider: "cursor", permissionMode: "plan", model: "cursor-fast", initialPrompt: "Review this lane." });
expect(launch.command).toBe("cursor-agent");
expect(launch.args).toEqual(expect.arrayContaining(["--mode", "plan", "--model", "cursor-fast"]));
expect(launch.args.at(-1)).toContain("Review this lane.");
expect(launch.initialInput).toBeUndefined();
expect(launch.initialInputDelayMs).toBeUndefined();
expect(launch.args).toEqual(["--mode", "plan", "--model", "cursor-fast"]);
expect(launch.initialInput).toContain("Review this lane.");
expect(launch.initialInputDelayMs).toBe(750);
});
});

Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/shared/cliLaunch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,17 +377,17 @@ export function buildTrackedCliLaunchCommand(args: {
}

if (args.provider === "cursor") {
const prompt = workTabCliPrompt(initialPrompt, skillRoots);
const cursorModel = resolveCursorCliModelForLaunch(args.model);
const commandArgs = [
...permissionModeToCursorFlags(permissionMode),
...modelToCliFlag(cursorModel),
prompt,
];
const initialInput = initialPrompt ? workTabCliPrompt(initialPrompt, skillRoots) : null;
return {
command: "cursor-agent",
args: commandArgs,
startupCommand: commandArrayToLine(["cursor-agent", ...commandArgs]),
...(initialInput ? { initialInput, initialInputDelayMs: 750 } : {}),
...(agentSkillEnv ? { env: agentSkillEnv } : {}),
};
}
Expand Down
32 changes: 15 additions & 17 deletions docs/features/terminals-and-sessions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,19 +360,20 @@ Renderer surfaces:
profile to the recorded `TerminalToolType` (`cursor-cli`, `droid`,
`opencode`, etc.) and the human tab title. `buildTrackedCliLaunchCommand`
returns a typed `TrackedCliLaunchCommand` (`{ command?, args,
startupCommand, env? }`) so `ptyService.create` can spawn tracked
startupCommand, initialInput?, initialInputDelayMs?, env? }`) so `ptyService.create` can spawn tracked
CLIs with explicit argv instead of typing the launch command into an
already-open shell. Cursor uses a direct `/bin/bash -lc` launch on
non-Windows because its startup path needs a multi-line preamble:
Cursor pre-allocates a chat with
`cursor-agent create-chat` so the resume target is known up front,
Droid materializes a temp `--settings` JSON keyed off the active
already-open shell. Cursor launches `cursor-agent` directly and keeps
empty Work launches idle; when ADE has an actual first user prompt,
it waits for Cursor's interactive prompt and submits the ADE guidance
plus user text through PTY input instead of argv. Droid materializes a
temp `--settings` JSON keyed off the active
permission mode, and OpenCode passes its inline permission policy
through the `OPENCODE_CONFIG_CONTENT` env var. ADE session guidance is
injected on every launch with skill roots resolved from the active
lane worktree when known: Claude gets `buildAdeCliAgentGuidance(...)`
through `--append-system-prompt`, while every other provider receives
a leading prompt from `buildAdeCliInlineGuidance(...)`. Launch env also
through `--append-system-prompt`; Codex, Droid, and OpenCode receive
a leading prompt from `buildAdeCliInlineGuidance(...)`; Cursor receives
that prompt only when there is an initial user message. Launch env also
carries `ADE_AGENT_SKILLS_DIRS` when skill roots are known, including
lane/user `.claude`, `.agents`, `.ade`, `.codex` skill dirs plus
bundled ADE resources.
Expand All @@ -391,15 +392,12 @@ Renderer surfaces:
`permissionMode: "auto"`); `validateLaunchProfilePermissionMode`
rejects `auto` for any non-Claude provider and rejects `config-toml`
for providers other than Codex and OpenCode. A launch that passes an
`initialPrompt` embeds it into the provider launch itself (Claude/
Codex/Droid argv, OpenCode `--prompt`, Cursor's pre-created resume
command), not as a post-create PTY write, so the first user message
opens the PTY and is submitted as the provider's real first turn
instead of becoming a half-typed shell line. Codex argv also
appends `codexNoisyLocalMcpDisableFlags` (`-c
mcp_servers.unityMCP.enabled=false`, `-c
mcp_servers.xcode.enabled=false`) for every non-`config-toml`
launch so unbundled local MCP servers do not auto-spawn under ADE.
`initialPrompt` embeds it into the provider launch itself for
argv-oriented runtimes (Claude/Codex legacy prompt models/Droid,
OpenCode `--prompt`), while Codex interactive launches and Cursor use
`initialInput` after PTY readiness so the first user message is
submitted as the provider's real first turn instead of becoming a
half-typed shell line.
Plain "shell" launches and `resolveCleanShellLaunchFields({
platform, shell, comSpec })` together produce a deterministic
argv/env per OS that skips the user's profile / rc / config files
Expand Down
5 changes: 4 additions & 1 deletion docs/features/terminals-and-sessions/pty-and-processes.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,14 +432,17 @@ write paths into one call:
the command line so the continuation honours the user's current
model picker. For the first ended-session continuation with
structured `resumeMetadata`, `text` is also passed as the provider
prompt argument.
prompt argument, except for Cursor where ADE waits for the interactive
prompt and writes the text through PTY input.
4. De-duplicate concurrent sends through `resumeRuntimeFlights` (one
in-flight continuation per session id) so rapid sends do not spawn
parallel PTYs against the same row.
5. Spawn the continuation through `service.create({ sessionId, ... })`
in the same row. When the row has structured `resumeMetadata` and
no other resume flight is already running, the prompt is included in
the provider resume command and no follow-up PTY write is attempted.
Cursor is the exception: its continuation command stays prompt-free,
then the text is submitted after the resumed CLI is input-ready.
OpenCode uses its replay-resume command when the installed CLI
supports it. If the code has to reuse an already-started resume
flight, it writes `text` after the PTY is attached. The return shape
Expand Down
14 changes: 8 additions & 6 deletions docs/features/terminals-and-sessions/ui-surfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,19 +542,21 @@ Launch commands are built by `apps/desktop/src/shared/cliLaunch.ts`:
host-owned so ADE does not synthesize partial MCP tables that the
CLI rejects during config validation.
- **Cursor** → `--mode plan|ask` for read-only modes and `--force`
for full-auto. Sessions pre-allocate a chat id with
`cursor-agent create-chat` so `--resume <id>` is always known.
for full-auto. Fresh launches start interactive `cursor-agent`
directly; initial user prompts are submitted through PTY input after
Cursor readiness, and empty launches do not submit ADE guidance as a
first turn.
- **Droid** → an autonomy-tiered settings JSON written to a temp file
that `droid --settings $ADE_DROID_SETTINGS` consumes; `spec`
autonomy is the plan/read-only fallback.
- **OpenCode** → an inline JSON permission policy passed via the
`OPENCODE_CONFIG_CONTENT` env var (`config-toml` mode skips the env
so OpenCode reads `opencode.json` instead). Plan mode adds `--agent
plan`.
Every provider also receives the ADE CLI guidance prompt — Claude
through `--append-system-prompt`, every other provider as a leading
prompt argument — so the agent starts with the ADE wrappers in
context.
Every provider also receives ADE CLI guidance — Claude through
`--append-system-prompt`, Codex/Droid/OpenCode as a leading prompt
argument, and Cursor through PTY `initialInput` only when there is an
initial user prompt.
- `buildTrackedCliStartupCommand({ provider, permissionMode, ... })`
thin wrapper that returns just the shell-typed `startupCommand`.
- `resolveTrackedCliResumeCommand(session)` — internal runtime helper
Expand Down
Loading