diff --git a/apps/ade-cli/src/adeRpcServer.test.ts b/apps/ade-cli/src/adeRpcServer.test.ts index 9d39c2d2c..47ed66aee 100644 --- a/apps/ade-cli/src/adeRpcServer.test.ts +++ b/apps/ade-cli/src/adeRpcServer.test.ts @@ -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" }); @@ -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(); }); diff --git a/apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts b/apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts index 66a28af5c..be504d1af 100644 --- a/apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts +++ b/apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts @@ -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", @@ -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(); }); diff --git a/apps/desktop/src/renderer/components/chat/AgentChatPane.test.tsx b/apps/desktop/src/renderer/components/chat/AgentChatPane.test.tsx index adeaf3eca..f11629946 100644 --- a/apps/desktop/src/renderer/components/chat/AgentChatPane.test.tsx +++ b/apps/desktop/src/renderer/components/chat/AgentChatPane.test.tsx @@ -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 () => { @@ -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 () => { diff --git a/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts b/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts index f7a960338..782c3941d 100644 --- a/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts +++ b/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts @@ -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", () => { @@ -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); }); }); diff --git a/apps/desktop/src/shared/cliLaunch.ts b/apps/desktop/src/shared/cliLaunch.ts index c89fc3952..d06de317a 100644 --- a/apps/desktop/src/shared/cliLaunch.ts +++ b/apps/desktop/src/shared/cliLaunch.ts @@ -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 } : {}), }; } diff --git a/docs/features/terminals-and-sessions/README.md b/docs/features/terminals-and-sessions/README.md index 2a03229ff..4def0ff77 100644 --- a/docs/features/terminals-and-sessions/README.md +++ b/docs/features/terminals-and-sessions/README.md @@ -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. @@ -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 diff --git a/docs/features/terminals-and-sessions/pty-and-processes.md b/docs/features/terminals-and-sessions/pty-and-processes.md index 92cba9b15..aca355db2 100644 --- a/docs/features/terminals-and-sessions/pty-and-processes.md +++ b/docs/features/terminals-and-sessions/pty-and-processes.md @@ -432,7 +432,8 @@ 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. @@ -440,6 +441,8 @@ write paths into one call: 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 diff --git a/docs/features/terminals-and-sessions/ui-surfaces.md b/docs/features/terminals-and-sessions/ui-surfaces.md index c22a68d78..63127a770 100644 --- a/docs/features/terminals-and-sessions/ui-surfaces.md +++ b/docs/features/terminals-and-sessions/ui-surfaces.md @@ -542,8 +542,10 @@ 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 ` 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. @@ -551,10 +553,10 @@ Launch commands are built by `apps/desktop/src/shared/cliLaunch.ts`: `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