diff --git a/apps/ade-cli/README.md b/apps/ade-cli/README.md index c77d4ed96..ea59aac23 100644 --- a/apps/ade-cli/README.md +++ b/apps/ade-cli/README.md @@ -291,15 +291,12 @@ ade prs comments pr-id --text ade run defs --text ade run start web --lane lane-id ade shell start --lane lane-id -- npm test -ade shell start-cli codex --lane lane-id --permission-mode edit --message "fix failing tests" -ade shell start-cli claude --lane lane-id --model anthropic/claude-opus-4-8 --reasoning-effort ultracode --prompt "fix failing tests" -ade shell start-cli --provider claude --lane lane-id --permission-mode default +ade new chat --mode chat --lane lane-id --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --no-fast --permissions full-auto --prompt "fix failing tests" +ade new chat --mode cli --lane lane-id --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --no-fast --permissions full-auto --prompt "fix failing tests" +ade new chat --mode chat --lane auto --lane-name fix-checkout-flow --prompt "fix failing tests" ade chat list --lane lane-id --include-automation --no-archived --text -ade chat create --lane lane-id --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --no-fast --permissions full-auto -ade chat create --lane lane-id --provider claude --model anthropic/claude-opus-4-8 --prompt "fix failing tests" ade chat create --lane lane-id --provider codex --model openai/gpt-5.5 --permissions full-auto --print-config --json ade chat read session-id --limit 20 --text -ade agent spawn --lane lane-id --provider codex --model openai/gpt-5.5 --permissions full-auto --prompt "fix failing tests" ade code ade code --embedded ade tests run --lane lane-id --suite unit --wait diff --git a/apps/ade-cli/src/adeRpcServer.test.ts b/apps/ade-cli/src/adeRpcServer.test.ts index de90ebd21..d647af9e6 100644 --- a/apps/ade-cli/src/adeRpcServer.test.ts +++ b/apps/ade-cli/src/adeRpcServer.test.ts @@ -1636,6 +1636,7 @@ describe("adeRpcServer", () => { initialInput: "fix failing tests", modelId: "openai/gpt-5.5", reasoningEffort: "xhigh", + fastMode: false, cols: 90, rows: 24, }); @@ -1656,7 +1657,7 @@ describe("adeRpcServer", () => { }), ); const createCall = fixture.runtime.ptyService.create.mock.calls.at(-1)?.[0]; - expect(createCall?.args).toEqual(expect.arrayContaining(["--model", "gpt-5.5", "-c", "model_reasoning_effort=\"xhigh\""])); + expect(createCall?.args).toEqual(expect.arrayContaining(["--model", "gpt-5.5", "-c", "model_reasoning_effort=\"xhigh\"", "-c", "service_tier=\"default\""])); expect(createCall?.args).not.toContain(expect.stringContaining("fix failing tests")); expect(createCall?.initialInput).toContain("fix failing tests"); expect(createCall?.initialInputDelayMs).toBe(750); diff --git a/apps/ade-cli/src/adeRpcServer.ts b/apps/ade-cli/src/adeRpcServer.ts index d3d61902a..1b666a443 100644 --- a/apps/ade-cli/src/adeRpcServer.ts +++ b/apps/ade-cli/src/adeRpcServer.ts @@ -313,6 +313,8 @@ const TOOL_SPECS: ToolSpec[] = [ model: { type: "string" }, modelId: { type: "string" }, reasoningEffort: { type: "string" }, + fastMode: { type: "boolean" }, + codexFastMode: { type: "boolean", deprecated: true }, cwd: { type: "string" }, chatSessionId: { type: "string" }, tracked: { type: "boolean", default: true } @@ -1663,6 +1665,10 @@ function asBoolean(value: unknown, fallback = false): boolean { return typeof value === "boolean" ? value : fallback; } +function asOptionalBoolean(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined; +} + function asPositiveInteger(value: unknown): number | null { let parsed = NaN; if (typeof value === "number") parsed = value; @@ -3473,6 +3479,7 @@ async function runTool(args: { const initialInput = asOptionalTrimmedString(toolArgs.initialInput)?.slice(0, 20_000) ?? null; const model = asOptionalTrimmedString(toolArgs.model) ?? asOptionalTrimmedString(toolArgs.modelId); const reasoningEffort = asOptionalTrimmedString(toolArgs.reasoningEffort); + const fastMode = asOptionalBoolean(toolArgs.fastMode) ?? asOptionalBoolean(toolArgs.codexFastMode); const initialInputMeta = deriveTrackedCliInitialInputSessionMeta({ provider, title: asOptionalTrimmedString(toolArgs.title), @@ -3498,6 +3505,7 @@ async function runTool(args: { sessionId: preassignedSessionId, model, reasoningEffort, + fastMode, initialPrompt: initialInput, laneWorktreePath, }); diff --git a/apps/ade-cli/src/cli.test.ts b/apps/ade-cli/src/cli.test.ts index 701c4c940..9787f2519 100644 --- a/apps/ade-cli/src/cli.test.ts +++ b/apps/ade-cli/src/cli.test.ts @@ -1132,6 +1132,161 @@ describe("ADE CLI", () => { }); }); + it("builds new chat mode with auto-created lane and kickoff prompt", () => { + const plan = buildCliPlan([ + "new", + "chat", + "--mode", + "chat", + "--lane", + "auto", + "--lane-name", + "fix-login", + "--base", + "origin/main", + "--provider", + "codex", + "--model", + "openai/gpt-5.5", + "--reasoning-effort", + "xhigh", + "--permissions", + "full-auto", + "--no-fast", + "--prompt", + "Fix login", + "--arg", + "openInUi=true", + ]); + + const executePlan = expectExecutePlan(plan); + expect(executePlan.steps).toHaveLength(3); + expect(executePlan.steps[0]?.params).toEqual({ + name: "create_lane", + arguments: { + name: "fix-login", + baseBranch: "origin/main", + }, + }); + + const createParams = (executePlan.steps[1]?.params as (v: Record) => Record)({ + lane: { id: "lane-new" }, + }); + expect(createParams).toMatchObject({ + name: "run_ade_action", + arguments: { + domain: "chat", + action: "createSession", + args: { + laneId: "lane-new", + provider: "codex", + model: "openai/gpt-5.5", + modelId: "openai/gpt-5.5", + reasoningEffort: "xhigh", + permissionMode: "full-auto", + fastMode: false, + codexFastMode: false, + openInUi: true, + }, + }, + }); + + const sendParams = (executePlan.steps[2]?.params as (v: Record) => Record)({ + session: { domain: "chat", action: "createSession", result: { sessionId: "chat-new" } }, + }); + expect(sendParams).toMatchObject({ + name: "run_ade_action", + arguments: { + domain: "chat", + action: "sendMessage", + args: { + sessionId: "chat-new", + text: "Fix login", + }, + }, + }); + }); + + it("builds new chat CLI mode with the same launch controls", () => { + const plan = buildCliPlan([ + "new", + "chat", + "--mode", + "cli", + "--lane", + "lane-1", + "--provider", + "Codex", + "--model", + "openai/gpt-5.5", + "--reasoning-effort", + "xhigh", + "--permissions", + "full-auto", + "--no-fast", + "--prompt", + "Fix the tests", + ]); + + const executePlan = expectExecutePlan(plan); + expect(executePlan.label).toBe("new chat cli"); + expect(executePlan.steps).toHaveLength(1); + const launchParams = (executePlan.steps[0]?.params as (v: Record) => Record)({}); + expect(launchParams).toMatchObject({ + name: "start_cli_session", + arguments: { + laneId: "lane-1", + provider: "codex", + model: "openai/gpt-5.5", + modelId: "openai/gpt-5.5", + reasoningEffort: "xhigh", + permissionMode: "full-auto", + fastMode: false, + codexFastMode: false, + initialInput: "Fix the tests", + cols: 120, + rows: 36, + tracked: true, + }, + }); + }); + + it("rejects unknown providers for new chat before launching", () => { + expect(() => + buildCliPlan([ + "new", + "chat", + "--lane", + "lane-1", + "--provider", + "mystery", + ]), + ).toThrow(/Provider must be claude, codex, cursor, droid, opencode, or shell/); + }); + + it("does not treat new --mode values as subcommands", () => { + const plan = buildCliPlan([ + "new", + "--mode", + "cli", + "--lane", + "lane-1", + "--prompt", + "Fix the tests", + ]); + + const executePlan = expectExecutePlan(plan); + const launchParams = (executePlan.steps[0]?.params as (v: Record) => Record)({}); + expect(launchParams).toMatchObject({ + name: "start_cli_session", + arguments: { + laneId: "lane-1", + provider: "codex", + initialInput: "Fix the tests", + }, + }); + }); + it("prints chat create config without launching a session", () => { const plan = buildCliPlan([ "chat", @@ -1592,6 +1747,30 @@ describe("ADE CLI", () => { kickoff: { ok: true, accepted: true, sessionId: "chat-new" }, }); + const newChatWithLaneAndKickoff = summarizeExecution({ + plan: { kind: "execute", label: "new chat", steps: [] }, + connection: chatConnection, + values: { + lane: { id: "lane-new", name: "fix-login" }, + session: { + domain: "chat", + action: "createSession", + result: { sessionId: "chat-new" }, + }, + result: { + domain: "chat", + action: "sendMessage", + result: { ok: true, accepted: true, sessionId: "chat-new" }, + }, + }, + } as any); + expect(newChatWithLaneAndKickoff).toEqual({ + ok: true, + lane: { id: "lane-new", name: "fix-login" }, + session: { sessionId: "chat-new" }, + kickoff: { ok: true, accepted: true, sessionId: "chat-new" }, + }); + const chatRead = summarizeExecution({ plan: { kind: "execute", label: "chat read", steps: [] }, connection: chatConnection, @@ -2667,13 +2846,14 @@ describe("ADE CLI", () => { expect(chatCreateHelp.text).toContain("--reasoning-effort"); expect(chatCreateHelp.text).toContain("ultracode"); expect(chatCreateHelp.text).toContain("--prompt "); - expect(chatCreateHelp.text).toContain("ade shell start-cli claude"); + expect(chatCreateHelp.text).toContain("ade new chat --mode cli"); expect(chatCreateHelp.text).toContain("codexSandbox=danger-full-access"); const chatHelp = buildCliPlan(["help", "chat"]); expect(chatHelp.kind).toBe("help"); if (chatHelp.kind !== "help") return; expect(chatHelp.text).toContain("ade chat read "); + expect(chatHelp.text).toContain("ade new chat --mode cli"); const agentSpawnHelp = buildCliPlan(["agent", "spawn", "--help"]); expect(agentSpawnHelp.kind).toBe("help"); @@ -3539,6 +3719,7 @@ describe("ADE CLI", () => { "gpt-5.4", "--reasoning", "high", + "--no-fast", "--message", "fix the tests", ]); @@ -3551,6 +3732,7 @@ describe("ADE CLI", () => { provider: "codex", model: "gpt-5.4", reasoningEffort: "high", + fastMode: false, initialInput: "fix the tests", }), }); diff --git a/apps/ade-cli/src/cli.ts b/apps/ade-cli/src/cli.ts index 7a9684f02..2f8e57f68 100644 --- a/apps/ade-cli/src/cli.ts +++ b/apps/ade-cli/src/cli.ts @@ -22,6 +22,7 @@ import { runSkillCommand, } from "./commands/skill"; import { buildDeeplink } from "../../desktop/src/shared/deeplinks"; +import { deriveDeterministicLaneNameFromPrompt } from "../../desktop/src/shared/laneNameFallback"; import { AUTOMATIONS_COMING_SOON_MESSAGE, readAutomationsEnvOverride, @@ -455,6 +456,7 @@ const TOP_LEVEL_HELP = `${ADE_BANNER} $ ade help Display help for a command $ ade auth status Check local ADE CLI readiness $ ade code Open ADE Work chat in the terminal + $ ade new chat --mode chat|cli --prompt "fix" Start an ADE Work chat or tracked CLI session $ ade desktop Launch the installed desktop app $ ade open Open an ade:// or ade-app.dev deeplink via the OS $ ade link lane | session | branch | pr | linear-issue @@ -479,7 +481,6 @@ const TOP_LEVEL_HELP = `${ADE_BANNER} $ ade terminal list | read | write | signal Control an attached session terminal $ ade history list | show | commits | export Inspect ADE operation timeline and lane commits $ ade chat list | create | send | interrupt Work with ADE agent chats - $ ade agent spawn --lane --prompt Launch an agent session in ADE $ ade cto state | chats Operate CTO state and Work chats $ ade linear graphql | workflows | run | sync Operate Linear GraphQL, routing, and sync workflows $ ade github app-auth login | status | clear Authorize the machine ADE GitHub App (device flow) @@ -526,6 +527,8 @@ const TOP_LEVEL_HELP = `${ADE_BANNER} $ ade --socket browser open http://localhost:5173 --new-tab --text $ ade terminal read --chat-session --text $ ade terminal read --pty --text + $ ade new chat --mode chat --lane --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --permissions full-auto --no-fast --prompt "Fix the tests" + $ ade new chat --mode cli --lane --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --permissions full-auto --no-fast --prompt "Fix the tests" Generic ADE action JSON contract: Object-shaped call: @@ -576,6 +579,7 @@ function helpKeyWithSubcommand(primaryKey: string, args: readonly string[]): str const normalizedSubcommand = subcommand.toLowerCase(); if (primaryKey === "chat" && normalizedSubcommand === "spawn") return "chat create"; if (primaryKey === "agent" && normalizedSubcommand === "start") return "agent spawn"; + if (primaryKey === "new" && normalizedSubcommand === "cli") return "new chat"; return `${primaryKey} ${normalizedSubcommand}`; } @@ -1166,6 +1170,50 @@ const HELP_BY_COMMAND: Record = { ? Help when it is the first prompt character / Command palette `, + new: `${ADE_BANNER} + New ADE work session + + Start either a persistent ADE Work chat or a tracked provider CLI session + with one command. This mirrors the desktop New Chat mode toggle. + + $ ade new chat --mode chat --lane --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --permissions full-auto --no-fast --prompt "Fix the tests" + $ ade new chat --mode cli --lane --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --permissions full-auto --no-fast --prompt "Fix the tests" + $ ade new chat --mode chat --auto-create-lane --prompt "Fix login" + $ ade new cli --lane --provider claude --model anthropic/claude-opus-4-8 --effort ultracode --prompt "Review the diff" + + Flags: + --mode Select a persistent ADE chat or tracked provider CLI session. + --lane Target lane. "auto" is the same as --auto-create-lane. + --auto-create-lane Create a new lane first, then launch there. + --lane-name Explicit name for an auto-created lane. + --base Optional base branch for an auto-created lane. + --provider claude | codex | cursor | droid | opencode. CLI mode also accepts shell. + --model Runtime model id. + --reasoning-effort Reasoning tier. Alias: --effort. + --permissions default | auto | plan | edit | full-auto | config-toml. + --fast Request fast service tier when supported. + --no-fast, --standard Disable fast service tier explicitly. + --prompt First chat message or CLI initial input. + + Compatibility: + ade chat create still creates persistent Work chats. + ade shell start-cli still starts tracked provider CLI sessions. + ade agent spawn is the older agent launcher and should not be used for new flows. +`, + "new chat": `${ADE_BANNER} + New chat / CLI session + + One entry point for ADE's desktop New Chat toggle: + --mode chat creates a persistent ADE Work chat. + --mode cli starts a tracked provider CLI terminal. + + $ ade new chat --mode chat --lane --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --permissions full-auto --no-fast --prompt "Fix the tests" + $ ade new chat --mode cli --lane --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --permissions full-auto --no-fast --prompt "Fix the tests" + $ ade new chat --mode chat --lane auto --lane-name fix-login --prompt "Fix login" + + The command defaults to the current ADE lane when ADE_LANE_ID is set. Use + --auto-create-lane or --lane auto to create a lane before launching. +`, lanes: `${ADE_BANNER} Lanes @@ -1319,6 +1367,7 @@ const HELP_BY_COMMAND: Record = { $ ade shell start --lane -- npm test Start a tracked shell session $ ade shell start --lane -c "npm test" Start with a command string + $ ade new chat --mode cli --lane --provider codex --permission-mode edit --prompt "fix tests" $ ade shell start-cli codex --lane --permission-mode edit $ ade shell start-cli claude --lane --reasoning-effort ultracode --prompt "fix tests" $ ade shell start --provider claude --lane --message "fix tests" @@ -1330,8 +1379,8 @@ const HELP_BY_COMMAND: Record = { After start, use the returned session id with: $ ade terminal read --terminal --text - Use start-cli for tracked provider CLI sessions. It supports --reasoning-effort - for reasoning-aware providers; ade agent spawn is the legacy CLI-session path. + Prefer ade new chat --mode cli for new tracked provider CLI sessions. start-cli + remains as the compatibility command behind that mode. `, terminal: `${ADE_BANNER} Attached terminal @@ -1376,8 +1425,8 @@ const HELP_BY_COMMAND: Record = { $ ade chat create --from-linear-issue ENG-431 Start a chat with an attached issue + kickoff (alias: --linear-issue-json) $ ade chat send --text "next step" Send a message $ ade chat read --limit 20 --text Read recent chat messages - $ ade shell start-cli claude --lane --reasoning-effort ultracode --prompt "fix" - Start a tracked Claude Code CLI session + $ ade new chat --mode cli --lane --provider claude --reasoning-effort ultracode --prompt "fix" + Start a tracked provider CLI session $ ade chat attach-linear-issue --issue-id ENG-431 Attach a Linear issue to a chat/CLI session $ ade chat detach-linear-issue [--issue-id ENG-431] @@ -1385,7 +1434,8 @@ const HELP_BY_COMMAND: Record = { $ ade chat linear-issues --text List issues attached to a session $ ade chat interrupt Stop an active turn $ ade chat slash --text List slash commands for a session - $ ade agent spawn --lane --prompt "fix" Start a new agent work session + $ ade new chat --mode cli --lane --prompt "fix" + Start a tracked provider CLI session Create flags: --provider claude | codex | cursor | droid | opencode. @@ -1449,12 +1499,15 @@ const HELP_BY_COMMAND: Record = { $ ade actions run chat.getAvailableModels --input-json '{"provider":"codex"}' --json CLI sessions: - Use ade shell start-cli claude ... --reasoning-effort when you want - a tracked Claude Code terminal session instead of a persistent Work chat. + Use ade new chat --mode cli ... when you want a tracked provider CLI terminal + instead of a persistent Work chat. `, agent: `${ADE_BANNER} Agent sessions + Compatibility path for older agent launches. Prefer: + $ ade new chat --mode cli --lane --provider codex --prompt "Fix the failing test" + $ ade agent spawn --lane --prompt "Fix the failing test" $ ade agent spawn --lane --provider codex --model openai/gpt-5.5 --permissions full-auto $ ade agent spawn --lane --context-file docs/context.md --prompt "continue" @@ -1471,16 +1524,15 @@ const HELP_BY_COMMAND: Record = { Reasoning effort: ade agent spawn launches the older CLI-session agent tool and does not - support reasoning effort. Use ade chat create for persistent Work chats, - or ade shell start-cli ... --reasoning-effort for tracked CLI - sessions that support reasoning tiers. + support reasoning effort. Use ade new chat --mode chat for persistent Work + chats or ade new chat --mode cli for tracked CLI sessions. `, "agent spawn": `${ADE_BANNER} Agent spawn - Launch a Codex or Claude CLI-session agent in a lane-scoped tracked terminal. - This is not the same as chat.createSession; it does not persist chat runtime - settings such as reasoning effort. + Compatibility path for older Codex or Claude CLI-session agents. Prefer + ade new chat --mode cli, which supports the desktop New Chat CLI mode and + reasoning/fast launch settings. This command does not support reasoning effort. $ ade agent spawn --lane --provider codex --model openai/gpt-5.5 --permissions full-auto --prompt "Fix the failing test" $ ade agent spawn --lane --provider claude --model claude-opus-4-8 --permissions plan --prompt "Review the diff" @@ -3548,13 +3600,7 @@ function buildLanePlan(args: string[]): CliPlan { */ function readChatLaunchConfig(args: string[]): JsonObject { const modelArg = readValue(args, ["--model", "--model-id"]); - const fastRequested = readFlag(args, ["--fast", "--codex-fast"]); - const standardRequested = readFlag(args, ["--standard", "--no-fast", "--no-codex-fast"]); - if (fastRequested && standardRequested) { - throw new CliUsageError( - "Use either --fast/--codex-fast or --standard/--no-fast/--no-codex-fast, not both.", - ); - } + const fastMode = readFastModeFlag(args); const config: JsonObject = {}; maybePut(config, "provider", readValue(args, ["--provider"])); maybePut(config, "model", modelArg); @@ -3565,18 +3611,281 @@ function readChatLaunchConfig(args: string[]): JsonObject { "permissionMode", readValue(args, ["--permission-mode", "--permissions"]), ); - if (fastRequested) { - config.fastMode = true; - // Mirror to the deprecated alias so older daemons (pre-rename) still see --fast. - config.codexFastMode = true; - } - if (standardRequested) { - config.fastMode = false; - config.codexFastMode = false; + if (fastMode !== undefined) { + config.fastMode = fastMode; + // Mirror to the deprecated alias so older daemons (pre-rename) still see the selection. + config.codexFastMode = fastMode; } return config; } +function readFastModeFlag(args: string[]): boolean | undefined { + const fastRequested = readFlag(args, ["--fast", "--codex-fast"]); + const standardRequested = readFlag(args, [ + "--standard", + "--no-fast", + "--no-codex-fast", + ]); + if (fastRequested && standardRequested) { + throw new CliUsageError( + "Use either --fast/--codex-fast or --standard/--no-fast/--no-codex-fast, not both.", + ); + } + return fastRequested ? true : standardRequested ? false : undefined; +} + +function autoLaneGenericSuffix(date = new Date()): string { + const pad = (value: number) => String(value).padStart(2, "0"); + return [ + date.getFullYear(), + pad(date.getMonth() + 1), + pad(date.getDate()), + ].join("") + "-" + [ + pad(date.getHours()), + pad(date.getMinutes()), + pad(date.getSeconds()), + ].join(""); +} + +function readNewChatPrompt(args: string[]): string | null { + const promptArgs = takeArgsAfterTerminator(args); + const prompt = promptArgs + ? promptArgs.join(" ").trim() + : readValue(args, ["--prompt", "--message", "--initial-input", "--kickoff"]); + return prompt?.trim() || null; +} + +function isAutoLaneSelector(value: string | null | undefined): boolean { + if (!value) return false; + const normalized = value.trim().toLowerCase(); + return ( + normalized === "auto" || + normalized === "new" || + normalized === "auto-create" || + normalized === "__ade_auto_create_lane__" + ); +} + +function readNewChatMode(args: string[], defaultMode: "chat" | "cli"): "chat" | "cli" { + const explicitMode = readValue(args, ["--mode", "--kind"])?.trim().toLowerCase(); + const chatFlag = readFlag(args, ["--chat"]); + const cliFlag = readFlag(args, ["--cli", "--terminal"]); + const requested = explicitMode ?? (chatFlag ? "chat" : cliFlag ? "cli" : defaultMode); + if (chatFlag && cliFlag) { + throw new CliUsageError("Use either --chat or --cli, not both."); + } + if (explicitMode && (chatFlag || cliFlag)) { + const flagMode = chatFlag ? "chat" : "cli"; + if (explicitMode !== flagMode) { + throw new CliUsageError("Use one mode selector: --mode chat|cli, --chat, or --cli."); + } + } + if (requested !== "chat" && requested !== "cli") { + throw new CliUsageError("--mode must be either chat or cli."); + } + return requested; +} + +function resolveNewChatLaneArgs(args: string[], prompt: string | null): { + laneId: string | null; + autoCreateLane: boolean; + createLaneArgs: JsonObject | null; +} { + const laneValue = readLaneId(args); + const autoCreateLane = + readFlag(args, ["--auto-create-lane", "--create-lane", "--new-lane"]) || + isAutoLaneSelector(laneValue); + if (!autoCreateLane) { + const laneId = laneValue ?? process.env.ADE_LANE_ID ?? null; + if (!laneId) { + throw new CliUsageError("Provide --lane or --auto-create-lane."); + } + return { laneId, autoCreateLane: false, createLaneArgs: null }; + } + + const explicitName = readValue(args, ["--lane-name", "--name"]); + const namingSeed = prompt || explicitName || "New chat task"; + const laneName = + explicitName?.trim() || + deriveDeterministicLaneNameFromPrompt(namingSeed, { + genericSuffix: autoLaneGenericSuffix(), + }); + const createLaneArgs: JsonObject = { name: laneName }; + maybePut(createLaneArgs, "description", readValue(args, ["--description", "--desc"])); + maybePut(createLaneArgs, "baseBranch", readValue(args, ["--base", "--base-branch"])); + maybePut(createLaneArgs, "branchName", readValue(args, ["--branch-name"])); + maybePut(createLaneArgs, "parentLaneId", readValue(args, ["--parent", "--parent-lane", "--parent-lane-id"])); + return { laneId: null, autoCreateLane: true, createLaneArgs }; +} + +function buildNewPlan(args: string[]): CliPlan { + const surface = firstStandalonePositional(args) ?? "chat"; + if (surface !== "chat" && surface !== "cli") { + throw new CliUsageError("ade new supports `chat` or `cli`. Try `ade new chat --mode chat|cli ...`."); + } + return buildNewChatPlan(args, surface === "cli" ? "cli" : "chat"); +} + +function buildNewChatPlan(args: string[], defaultMode: "chat" | "cli"): CliPlan { + const mode = readNewChatMode(args, defaultMode); + const prompt = readNewChatPrompt(args); + const lane = resolveNewChatLaneArgs(args, prompt); + const provider = readValue(args, ["--provider"])?.trim().toLowerCase() || "codex"; + const modelArg = readValue(args, ["--model", "--model-id"]); + const reasoningEffort = readValue(args, ["--reasoning-effort", "--effort", "--reasoning"]); + const permissionMode = readValue(args, ["--permission-mode", "--permissions"]); + const fastMode = readFastModeFlag(args); + const title = readValue(args, ["--title"]); + const printConfig = readFlag(args, ["--print-config", "--dry-run"]); + + if (!isLaunchProfile(provider)) { + throw new CliUsageError("Provider must be claude, codex, cursor, droid, opencode, or shell."); + } + if (mode === "chat" && provider === "shell") { + throw new CliUsageError("Chat mode provider must be claude, codex, cursor, droid, or opencode."); + } + if (mode === "cli") { + const effectivePermissionMode = permissionMode ?? "default"; + if (!isTrackedCliPermissionMode(effectivePermissionMode)) { + throw new CliUsageError( + "permissionMode must be one of default, auto, plan, edit, full-auto, or config-toml.", + ); + } + validateLaunchProfilePermissionMode(provider, effectivePermissionMode); + } + + const laneIdFor = (values: JsonObject): string => { + if (!lane.autoCreateLane && lane.laneId) return lane.laneId; + const createdLaneId = laneIdFromCreateLaneValue(values.lane); + if (!createdLaneId) { + throw new CliUsageError("ade new chat could not resolve the auto-created lane id."); + } + return createdLaneId; + }; + + const launchArgs = mode === "chat" + ? collectGenericObjectArgs(args, { + provider, + model: modelArg, + modelId: modelArg, + reasoningEffort, + permissionMode, + droidPermissionMode: readValue(args, [ + "--droid-permission-mode", + "--droid-autonomy", + "--autonomy", + ]), + title, + surface: readValue(args, ["--surface"]) ?? "work", + ...(fastMode !== undefined ? { fastMode, codexFastMode: fastMode } : {}), + }) + : collectGenericObjectArgs(args, { + provider, + permissionMode: permissionMode ?? "default", + title, + initialInput: prompt, + model: modelArg, + modelId: modelArg, + reasoningEffort, + ...(fastMode !== undefined ? { fastMode, codexFastMode: fastMode } : {}), + cols: readIntOption(args, ["--cols"], 120), + rows: readIntOption(args, ["--rows"], 36), + cwd: readValue(args, ["--cwd"]), + tracked: !readFlag(args, ["--untracked"]), + }); + + if (printConfig) { + return { + kind: "static", + formatter: "action-result", + value: { + ok: true, + dryRun: true, + action: "new.chat", + mode, + autoCreateLane: lane.autoCreateLane, + ...(lane.createLaneArgs ? { createLane: lane.createLaneArgs } : { laneId: lane.laneId }), + launch: compactPreviewObject(launchArgs), + ...(mode === "chat" && prompt ? { afterCreate: [{ action: "chat.sendMessage", text: prompt }] } : {}), + }, + }; + } + + const steps: InvocationStep[] = []; + if (lane.autoCreateLane) { + steps.push(actionCallStep("lane", "create_lane", lane.createLaneArgs ?? {})); + } + + if (mode === "cli") { + steps.push({ + key: "result", + method: "ade/actions/call", + params: (values) => ({ + name: "start_cli_session", + arguments: { + ...launchArgs, + laneId: laneIdFor(values), + }, + }), + unwrapToolResult: true, + }); + return { + kind: "execute", + label: "new chat cli", + formatter: "pty-create", + steps, + }; + } + + steps.push({ + key: prompt ? "session" : "result", + method: "ade/actions/call", + params: (values) => ({ + name: "run_ade_action", + arguments: { + domain: "chat", + action: "createSession", + args: { + ...launchArgs, + laneId: laneIdFor(values), + }, + }, + }), + unwrapToolResult: true, + }); + + if (prompt) { + steps.push({ + key: "result", + method: "ade/actions/call", + params: (values) => { + const targetSession = sessionIdFromCreateChatValue(values.session); + if (!targetSession) { + throw new CliUsageError("ade new chat could not resolve the new session id to send the prompt."); + } + return { + name: "run_ade_action", + arguments: { + domain: "chat", + action: "sendMessage", + args: { + sessionId: targetSession, + text: prompt, + }, + }, + }; + }, + unwrapToolResult: true, + }); + } + + return { + kind: "execute", + label: "new chat", + steps, + }; +} + function codexPermissionPreview(permissionMode: string): JsonObject | null { if (permissionMode === "config-toml") { return { codexConfigSource: "config-toml" }; @@ -5467,6 +5776,7 @@ function buildCliSessionStartPlan( model: readValue(args, ["--model"]), modelId: readValue(args, ["--model-id"]), reasoningEffort: readValue(args, ["--reasoning", "--reasoning-effort"]), + fastMode: readFastModeFlag(args), cols: readIntOption(args, ["--cols"], 120), rows: readIntOption(args, ["--rows"], 36), cwd: readValue(args, ["--cwd"]), @@ -5905,22 +6215,7 @@ function buildChatPlan(args: string[]): CliPlan { if (sub === "create" || sub === "spawn") { const modelArg = readValue(args, ["--model", "--model-id"]); const reasoningEffort = readValue(args, ["--reasoning-effort", "--effort"]); - const fastRequested = readFlag(args, ["--fast", "--codex-fast"]); - const standardRequested = readFlag(args, [ - "--standard", - "--no-fast", - "--no-codex-fast", - ]); - if (fastRequested && standardRequested) { - throw new CliUsageError( - "Use either --fast/--codex-fast or --standard/--no-fast/--no-codex-fast, not both.", - ); - } - const fastMode: boolean | undefined = fastRequested - ? true - : standardRequested - ? false - : undefined; + const fastMode = readFastModeFlag(args); // `--print` opts the session's app-server initialize handshake into // print-mode (suppresses delta notification streams). Must be set at create // time because the handshake runs once when the runtime starts. @@ -10073,6 +10368,7 @@ function buildCliPlan( quotas: "usage", skills: "skill", gh: "github", + create: "new", }; const primaryHelpKey = aliases[primary] ?? primary; if (hasHelpFlag(args)) { @@ -10191,6 +10487,9 @@ function buildCliPlan( if (primary === "projects" || primary === "project") { return buildProjectsPlan(args); } + if (primary === "new" || primary === "create") { + return buildNewPlan(args); + } if (primary === "sync") { return buildSyncPlan(args); } @@ -15435,6 +15734,21 @@ function summarizeExecution(args: { return summarizePrCreateResult(values.result ?? values); } + if ( + plan.label === "new chat" && + (values.session !== undefined || values.result !== undefined) + ) { + const session = values.session ?? values.result; + return { + ok: true, + ...(values.lane !== undefined ? { lane: unwrapActionEnvelope(values.lane) } : {}), + session: unwrapActionEnvelope(session), + ...(values.session !== undefined && values.result !== undefined + ? { kickoff: unwrapActionEnvelope(values.result) } + : {}), + }; + } + if ( (plan.label === "chat create" || plan.label === "chat create from Linear issue") && @@ -15795,7 +16109,7 @@ async function runCli( plan.kind === "runtime" || plan.kind === "serve" || (plan.kind === "execute" && - /^(agent spawn|chat create|shell start cli)\b/.test(plan.label)) + /^(agent spawn|chat create|new chat|shell start cli)\b/.test(plan.label)) ) { reseedBundledAdeSkillsForCli(); } diff --git a/apps/ade-cli/src/services/sync/syncRemoteCommandService.ts b/apps/ade-cli/src/services/sync/syncRemoteCommandService.ts index e0d277335..4567b8da5 100644 --- a/apps/ade-cli/src/services/sync/syncRemoteCommandService.ts +++ b/apps/ade-cli/src/services/sync/syncRemoteCommandService.ts @@ -661,6 +661,7 @@ function parseStartCliSessionArgs(value: Record): SyncStartCliS model: asTrimmedString(value.model), modelId: asTrimmedString(value.modelId), reasoningEffort: asTrimmedString(value.reasoningEffort), + fastMode: asOptionalBoolean(value.fastMode) ?? asOptionalBoolean(value.codexFastMode), }; } @@ -2201,6 +2202,7 @@ function registerWorkRemoteCommands({ args, register }: RemoteCommandRegistratio sessionId: preassignedSessionId, model: parsed.modelId ?? parsed.model ?? undefined, reasoningEffort: parsed.reasoningEffort ?? undefined, + fastMode: parsed.fastMode ?? undefined, initialPrompt: parsed.initialInput, laneWorktreePath: resolveLaneWorktreePathForSync(args, parsed.laneId), }); diff --git a/apps/desktop/resources/agent-skills/ade-cli-control-plane/SKILL.md b/apps/desktop/resources/agent-skills/ade-cli-control-plane/SKILL.md index 5dfe7d3af..d11fa2715 100644 --- a/apps/desktop/resources/agent-skills/ade-cli-control-plane/SKILL.md +++ b/apps/desktop/resources/agent-skills/ade-cli-control-plane/SKILL.md @@ -83,30 +83,41 @@ ade lanes link-linear-issue --linear-issue-json '{...}' Start work from an issue: ``` +ade new chat --mode chat --lane --provider codex --model --prompt "Work this issue" ade lanes create-from-linear --issue-id ENG-431 --start-chat --provider codex --model -ade chat create --from-linear-issue ENG-431 # chat with the issue attached + kickoff +ade chat create --from-linear-issue ENG-431 # compatibility path: chat with the issue attached + kickoff ``` ## Chat vs. CLI sessions -Use `ade chat create` for persistent ADE Work chats. `--prompt` creates the -chat and then sends that text as the first message; use `--print-config` / -`--dry-run` to verify both the create payload and the follow-up send before -launching. Use `ade chat send --text ...` for later messages and +Use `ade new chat` as the canonical launch command. It mirrors the desktop New +Chat mode toggle: + +``` +ade new chat --mode chat --lane --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --permissions full-auto --no-fast --prompt "Fix the issue" +ade new chat --mode cli --lane --provider codex --model openai/gpt-5.5 --reasoning-effort xhigh --permissions full-auto --no-fast --prompt "Fix the issue" +ade new chat --mode chat --lane auto --lane-name fix-issue --prompt "Fix the issue" +``` + +`--mode chat` creates a persistent ADE Work chat. `--mode cli` starts a tracked +provider CLI terminal. Both accept lane, provider, model, reasoning effort, +permission mode, fast/no-fast, and prompt flags. Use `--lane auto` or +`--auto-create-lane` when the desktop UI would use the auto-create lane row. + +Use `ade chat send --text ...` for later messages and `ade chat read --text` to confirm recent transcript messages. -Use `ade shell start-cli ` for tracked provider CLI sessions in a -terminal. This is the reasoning-aware CLI path, for example: +Compatibility commands still exist, but do not teach them as the first choice: ``` -ade shell start-cli claude --lane --model anthropic/claude-opus-4-8 --reasoning-effort ultracode --permissions full-auto --prompt "Fix the issue" +ade chat create --lane --provider codex --model --prompt "Fix" # persistent Work chat +ade shell start-cli codex --lane --model --prompt "Fix" # tracked provider CLI terminal ``` `ade agent spawn` is the older CLI-session launcher and rejects -`--reasoning-effort`; choose it only when you do not need to pin a reasoning -tier. Common reasoning tiers include `minimal`, `low`, `medium`, `high`, -`xhigh`, `max`, and `ultracode`; confirm model-specific support with -`ade actions run chat.modelCatalog --json`. +`--reasoning-effort`; avoid it for new flows. Common reasoning tiers include +`minimal`, `low`, `medium`, `high`, `xhigh`, `max`, and `ultracode`; confirm +model-specific support with `ade actions run chat.modelCatalog --json`. Report what you actually did back to the issue with `ade linear comment` as you progress — that comment is how reviewers and the issue's watchers see status. Use `ade help linear` for the full flag set. diff --git a/apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts b/apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts index fe3b710f8..10fcb9726 100644 --- a/apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts +++ b/apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts @@ -2282,6 +2282,7 @@ describe("createSyncRemoteCommandService", () => { initialInput: "fix the tests", modelId: "openai/gpt-5.5", reasoningEffort: "xhigh", + fastMode: false, cols: 70, rows: 24, })); @@ -2298,7 +2299,7 @@ describe("createSyncRemoteCommandService", () => { }), ); const createCall = ptyService.create.mock.calls.at(-1)?.[0]; - expect(createCall?.args).toEqual(expect.arrayContaining(["--model", "gpt-5.5", "-c", "model_reasoning_effort=\"xhigh\""])); + expect(createCall?.args).toEqual(expect.arrayContaining(["--model", "gpt-5.5", "-c", "model_reasoning_effort=\"xhigh\"", "-c", "service_tier=\"default\""])); expect(createCall?.args).not.toContain(expect.stringContaining("fix the tests")); expect(createCall?.initialInput).toContain("fix the tests"); expect(createCall?.initialInputDelayMs).toBe(750); diff --git a/apps/desktop/src/shared/types/sync.ts b/apps/desktop/src/shared/types/sync.ts index 103482e92..00648b2b9 100644 --- a/apps/desktop/src/shared/types/sync.ts +++ b/apps/desktop/src/shared/types/sync.ts @@ -764,6 +764,9 @@ export type SyncStartCliSessionArgs = { model?: string | null; modelId?: string | null; reasoningEffort?: string | null; + fastMode?: boolean | null; + /** @deprecated Use fastMode. Accepted for older callers. */ + codexFastMode?: boolean | null; }; export type SyncStartCliSessionResult = { diff --git a/apps/ios/ADE/Services/SyncService.swift b/apps/ios/ADE/Services/SyncService.swift index 5493a4841..70ba9064d 100644 --- a/apps/ios/ADE/Services/SyncService.swift +++ b/apps/ios/ADE/Services/SyncService.swift @@ -4907,6 +4907,7 @@ final class SyncService: ObservableObject { initialInput: String? = nil, modelId: String? = nil, reasoningEffort: String? = nil, + fastMode: Bool? = nil, cols: Int? = nil, rows: Int? = nil, targetProjectId: String? = nil, @@ -4931,6 +4932,9 @@ final class SyncService: ObservableObject { if let reasoningEffort, !reasoningEffort.isEmpty { args["reasoningEffort"] = reasoningEffort } + if let fastMode { + args["fastMode"] = fastMode + } if let cols, cols > 0 { args["cols"] = cols } diff --git a/apps/ios/ADE/Views/Hub/HubComposerDrawer.swift b/apps/ios/ADE/Views/Hub/HubComposerDrawer.swift index 8a2dd303b..f4334c971 100644 --- a/apps/ios/ADE/Views/Hub/HubComposerDrawer.swift +++ b/apps/ios/ADE/Views/Hub/HubComposerDrawer.swift @@ -718,6 +718,7 @@ struct HubComposerDrawer: View { initialInput: opener, modelId: modelId, reasoningEffort: cliReasoning, + fastMode: fastModeSupported ? codexFastMode : nil, cols: 48, rows: 24, targetProjectId: targetProjectId, diff --git a/apps/ios/ADE/Views/Work/WorkNewChatScreen.swift b/apps/ios/ADE/Views/Work/WorkNewChatScreen.swift index 59f7f5b80..89832aff2 100644 --- a/apps/ios/ADE/Views/Work/WorkNewChatScreen.swift +++ b/apps/ios/ADE/Views/Work/WorkNewChatScreen.swift @@ -652,6 +652,7 @@ struct WorkNewChatScreen: View { initialInput: opener, modelId: modelId, reasoningEffort: cliReasoningEffort, + fastMode: fastModeSupported ? codexFastMode : nil, cols: 48, rows: 24 ) diff --git a/docs/features/agents/README.md b/docs/features/agents/README.md index 8f206c064..461184e92 100644 --- a/docs/features/agents/README.md +++ b/docs/features/agents/README.md @@ -18,7 +18,7 @@ those surfaces. | `apps/desktop/src/main/services/cto/workerAdapterRuntimeService.ts` | Adapter lifecycle for supported worker adapter types. | | `apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts` | CTO-only tools for chat spawning, worker management, and Linear dispatch. | | `apps/desktop/src/main/services/agentTools/agentToolsService.ts` | Detects external CLI tools on PATH. | -| `apps/ade-cli/src/cli.ts` | Agent-focused `ade` command surface and text/JSON output formatters. Includes the `ade ios-sim` (alias `ade ios`, `ade simulator`) family — see [iOS Simulator feature](../ios-simulator/README.md), the `ade --socket app-control ...` driver for live Electron apps, and the `ade --socket browser ...` driver for the in-app browser. The browser CLI covers tabs/navigation (`browser panel`, `open`, `new-tab`, `switch`, `close`), agent sessions (`browser session start`, `browser sessions`, `browser session `, `--browser-session `), hidden-tab observations/actions (`observe --map`, `click/fill/clear-field/press/wait --handle`, `trace`, `proof`), and selection / inspect commands. `ade secrets list|get|set|delete` is the typed surface for encrypted project-scoped ADE secrets that agents may read when the user names a secret. `ade chat create --provider codex --model --reasoning-effort --no-fast --permissions full-auto --prompt "..."` starts a persistent Work chat with explicit provider settings and then sends the prompt as the first message; `--print-config`/`--dry-run` prints the resolved create payload, provider permission mapping, and any follow-up send before launching. `ade chat read --text` reads recent transcript messages after create/send. `ade agent spawn` remains the legacy lane-scoped CLI-session launcher for Codex/Claude and deliberately rejects reasoning-effort flags; use `ade chat create` for Work chats or `ade shell start-cli ... --reasoning-effort ` for tracked CLI sessions when a launch must pin reasoning. `ade shell start --lane --chat-session ` (or `ADE_CHAT_SESSION_ID` from the env) attaches a tracked shell to an existing chat so `ade --socket terminal read --chat-session "$ADE_CHAT_SESSION_ID" --text` resolves to it. `ade lanes link-linear-issue --linear-issue-json '{...}'` (aliases `link-linear`, `linear-link`) links one or more Linear issues to an existing lane with optional `--role`, `--source`, `--include-in-pr`/`--no-include-in-pr`, and `--close-on-merge` flags. | +| `apps/ade-cli/src/cli.ts` | Agent-focused `ade` command surface and text/JSON output formatters. Includes the `ade ios-sim` (alias `ade ios`, `ade simulator`) family — see [iOS Simulator feature](../ios-simulator/README.md), the `ade --socket app-control ...` driver for live Electron apps, and the `ade --socket browser ...` driver for the in-app browser. The browser CLI covers tabs/navigation (`browser panel`, `open`, `new-tab`, `switch`, `close`), agent sessions (`browser session start`, `browser sessions`, `browser session `, `--browser-session `), hidden-tab observations/actions (`observe --map`, `click/fill/clear-field/press/wait --handle`, `trace`, `proof`), and selection / inspect commands. `ade secrets list|get|set|delete` is the typed surface for encrypted project-scoped ADE secrets that agents may read when the user names a secret. `ade new chat --mode chat|cli --lane --provider codex --model --reasoning-effort --no-fast --permissions full-auto --prompt "..."` mirrors the desktop New Chat toggle: `--mode chat` creates a persistent Work chat and sends the prompt as the first message, while `--mode cli` starts a tracked provider CLI terminal with the same provider/model/reasoning/permission/fast controls. `--print-config`/`--dry-run` previews the lane and launch payload before starting anything. `ade chat read --text` reads recent transcript messages after create/send. `ade chat create` and `ade shell start-cli` remain compatibility entry points; `ade agent spawn` is the older CLI-session launcher and deliberately rejects reasoning-effort flags. `ade shell start --lane --chat-session ` (or `ADE_CHAT_SESSION_ID` from the env) attaches a tracked shell to an existing chat so `ade --socket terminal read --chat-session "$ADE_CHAT_SESSION_ID" --text` resolves to it. `ade lanes link-linear-issue --linear-issue-json '{...}'` (aliases `link-linear`, `linear-link`) links one or more Linear issues to an existing lane with optional `--role`, `--source`, `--include-in-pr`/`--no-include-in-pr`, and `--close-on-merge` flags. | | `apps/ade-cli/src/adeRpcServer.ts` | Private ADE action RPC: registers actions, handles JSON-RPC, applies session-identity-based filtering, builds lane-scoped ADE guidance / `ADE_AGENT_SKILLS_DIRS` for worker CLI launches, and returns GitHub + ADE PR URLs from PR creation tools when available. | | `apps/desktop/resources/agent-skills/ade-cli-control-plane/SKILL.md` | Agent-facing ADE CLI control-plane guidance. It tells agents when to use normal shell commands vs. `ade`, distinguishes persistent Work chats from tracked CLI sessions, and keeps chat-attached terminals scoped to chat-visible logs/control rather than ordinary CLI command execution. | | `apps/desktop/src/main/services/cli/adeCliService.ts` | Desktop-side install / status / uninstall surface for the `ade` launcher. Owns the install-target path resolution and the optional shell-rc PATH append. | diff --git a/docs/features/sync-and-multi-device/remote-commands.md b/docs/features/sync-and-multi-device/remote-commands.md index 8f20e3940..c818912ee 100644 --- a/docs/features/sync-and-multi-device/remote-commands.md +++ b/docs/features/sync-and-multi-device/remote-commands.md @@ -290,7 +290,10 @@ A handful have more logic: `claude | codex | cursor | droid | opencode | shell` (any other value throws `"work.startCliSession requires provider."`), clamps `cols` to `[20, 240]` and `rows` to `[4, 120]`, and truncates - `initialInput` at 20 KB. Provider-specific argv, env, and shell + `initialInput` at 20 KB. `model` / `modelId`, `reasoningEffort`, + and `fastMode` flow into the same launch builder as desktop; the + older `codexFastMode` wire name is accepted only as a compatibility + alias. Provider-specific argv, env, and shell preambles come from `buildTrackedCliLaunchCommand` in `apps/desktop/src/shared/cliLaunch.ts` — the same module the desktop Work tab uses — so the runtime owns the