From fd4b197f830b284c28bb9281598e373e90cece3c Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Fri, 6 Nov 2026 10:58:42 -0800 Subject: [PATCH] fix(ui): hide runner selection during agent setup (#19517) ## Thinking Path > - Paperclip lets people manage AI agents for work. > - Agent setup connects a harness, model, account, and computer. > - The system already selects a qualified runner for a new agent. > - Setup still asks users to select that internal runner. > - The runner dropdown can be the only field in Advanced. > - This PR removes the dropdown and hides an empty Advanced section. > - Users keep their saved execution settings and see fewer setup controls. ## Linked Issues or Issue Description **What existing behavior does this improve?** Agent creation, onboarding, built-in agent setup, company import, and agent configuration. **Subsystem affected** The UI, setup error messages, tests, and Storybook fixtures. **Current behavior** Users can select a runner from a dropdown. Some setup screens place only that dropdown in Advanced. Errors can direct users to that control. **Proposed behavior** Remove runner dropdowns from setup and configuration. Use the existing automatic selection for new agents. Retain saved runner settings. Show Advanced only when other fields remain. **Reason and benefit** Users can configure agents without an extra internal execution choice. Setup also avoids an empty Advanced section. **Breaking changes** Users can no longer select a runner through these UI dropdowns. The API and CLI retain their runner options. Existing agents keep their saved execution settings. **Additional context** Related: [Default new agents to the qualified runner for their harness](https://github.com/paperclipai/paperclip/pull/15422). This PR changes the existing setup UI. It adds no harness or runtime default. The master merge retains the custom gateway placement from [#19510](https://github.com/paperclipai/paperclip/pull/19510). ## What Changed - Remove the shared runner dropdown and its setup/configuration callers. - Hide Advanced when it has no remaining fields. Keep command and argument settings where supported. Preserve custom gateway setup under Advanced on the Claude and OpenAI API key paths. - Keep setup failures visible and keep the retry action outside the removed control. - Update errors and documentation that referred to the removed dropdown. - Update component tests and Storybook fixtures for the new UI states. - Make Legacy first-task evals resume an API-configured setup record. Record that path separately from fresh automatic onboarding. ## Verification - Latest master integration (`187c0f6d6e5`): 276 onboarding/provider-connection tests passed across six files. UI typecheck and build passed. Token gates, query-error rendering checks, and the Storybook catalog check passed. The conflict resolution retains master's bounded connection tests and cancellation while removing the obsolete runner instruction. The first test command used the wrong working directory and failed before collecting tests; the corrected invocation from `ui/` passed. - Earlier branch verification passed: full workspace build and typecheck, 1,037 focused UI/service tests, 49 heartbeat provider contract tests, eval typecheck, and 30 map/catalog tests. - The full local `pnpm test:run` run passed all six chat integration shards (1,931 tests). It was stopped for the master merge before the complete suite finished. The [complete CI run](https://github.com/paperclipai/paperclip/actions/runs/42264662642) passed on the previous head `72e1699`. - Current head `a097c8369d079c839e78661a2206cfa62c5210e0`: [full CI](https://github.com/paperclipai/paperclip/actions/runs/42273168268) passed. PR checks total 55 passed and 3 skipped, with none failed or pending. Greptile reviewed this head at 5/5 with no actionable findings or unresolved review threads. - Live browser/provider evals have not run. The Legacy eval change has no live qualification claim. ## Risks - Removing the dropdown changes the setup UI. Existing API and CLI runner choices remain available. - Conditional Advanced sections must retain valid command and argument fields. - Legacy first-task evals now prove setup resumption. They do not prove the fresh-agent automatic default. - No schema change or data migration is included. ## Model Used OpenAI `gpt-6.1-sol`, through Codex in T3 Code. Reasoning effort: `xhigh`. The model used file editing, shell commands, and test tools. The harness does not expose the context window size. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-Authored-By: Paperclip --- doc/DEVELOPING.md | 3 +- doc/evals.md | 4 +- maps/agent-access-map/onboarding.md | 3 +- maps/feature-map/agents.md | 13 ++- maps/feature-map/onboarding.md | 8 +- maps/feature-map/runs-adapters.md | 3 +- .../services/agent-runner-selection.test.ts | 16 ++-- server/src/services/agent-runner-selection.ts | 8 +- .../heartbeat-runner-provider-config.test.ts | 2 +- .../native-runtime/provider-profile.ts | 2 +- .../native-runtime/setup-readiness.test.ts | 4 +- .../native-runtime/setup-readiness.ts | 4 +- tests/runner-e2e/README.md | 9 +- tests/runner-e2e/first-task-flow.ts | 24 ++++-- tests/runner-e2e/live-fixtures.ts | 2 +- .../AgentConfigForm.render.test.tsx | 48 ++++++----- ui/src/components/AgentConfigForm.tsx | 20 +++-- ui/src/components/AgentRunnerSelect.tsx | 44 ---------- .../ConfigureBuiltInAgentModal.test.tsx | 17 +--- .../components/ConfigureBuiltInAgentModal.tsx | 18 +--- ui/src/components/OnboardingWizard.test.tsx | 3 + ui/src/components/OnboardingWizard.tsx | 42 ++++------ ui/src/components/new-agent/NewAgentSetup.tsx | 17 ---- ui/src/lib/qualify-claude-setup.ts | 2 +- ui/src/pages/CompanyImport.test.tsx | 78 +++++++----------- ui/src/pages/CompanyImport.tsx | 2 +- ui/src/pages/NewAgent.test.tsx | 82 ++++++++----------- .../agent-settings-redesign.stories.tsx | 4 +- .../agents/codex-runner/creation.stories.tsx | 25 +++--- .../onboarding-agent-arc.stories.tsx | 17 ++-- 30 files changed, 209 insertions(+), 315 deletions(-) delete mode 100644 ui/src/components/AgentRunnerSelect.tsx diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 8eecc8fd780..814cb9750d0 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -2971,7 +2971,7 @@ runtimes on Apple Silicon, Intel Mac, and Linux x64 with supported glibc; Linux ARM64 remains Legacy until the pinned runtimes support it. Unsupported or unqualified targets retain Legacy. Claude over Darwin SSH keeps an automatic Legacy default; explicit native execution -requires matching remote assets. The runner override is in Advanced. Ordinary Claude models default to `claude-opus-5-5`; explicit models +requires matching remote assets. Agent setup and configuration select the runner automatically and do not expose a runner dropdown. Ordinary Claude models default to `claude-opus-5-5`; explicit models and provider routing remain authoritative. Existing agents keep their saved runner. Converting an existing Claude, Codex, OpenCode, or Pi agent selects its corresponding provider, preserves compatible models, credentials, workspace, and instructions, and resets execution sessions while retaining run history. @@ -3028,6 +3028,7 @@ them. Existing Pi agents retain their saved runner and native policies. Agent configuration uses the same section layout across adapters: model and provider belong to **Adapter**, environment variables have their own section, and command/extra arguments are folded under **Configuration → Advanced**. +Advanced is omitted when the selected adapter has no settings in that section. Lifecycle, timeout, and interrupt grace settings live under **Advanced Run Policy**. Permission selectors with a single valid mode are hidden; a saved unsupported mode still exposes remediation. diff --git a/doc/evals.md b/doc/evals.md index 2bd484459b0..ccbd4ec8d39 100644 --- a/doc/evals.md +++ b/doc/evals.md @@ -618,7 +618,9 @@ verified worker process loss, and multi-turn answers grounded in actual task records. See the [workflow and qualification limits](../tests/runner-e2e/README.md#remaining-native-agent-chat-qualification). The 26 native `first-task` cells inspect the production wizard's saved native default on qualified targets, without an API runner switch. The 26 Legacy cells -select Legacy explicitly in Advanced. Live results and semantic answer reviews must accompany +use an API-configured inactive setup record and resume it in the wizard, as +described in the [first-task setup contract](../tests/runner-e2e/README.md#first-task-onboarding). +Live results and semantic answer reviews must accompany any qualification claim; catalog presence alone is not a pass. The explicit-only [native question/resume qualification](../tests/runner-e2e/README.md#task-continuation) diff --git a/maps/agent-access-map/onboarding.md b/maps/agent-access-map/onboarding.md index 9c241a3fe7e..c9ce0721d01 100644 --- a/maps/agent-access-map/onboarding.md +++ b/maps/agent-access-map/onboarding.md @@ -88,7 +88,8 @@ Policy: Instance setup is operator work; company creation is board; agent creati creation retain their individual permissions. The wizard selects the Codex harness and lets the server resolve an automatic -new-agent runner; explicit Legacy remains available. Supported-platform and +new-agent runner without a runner dropdown. Explicit runner requests remain +available through the agent creation API. Supported-platform and immutable sandbox evidence follow [environment probe policy](./execution-environments.md#probe). Creation/discovery do not provision a target to determine this default. Adapter setup validation requires `agents:create`, plus saved-agent update access when diff --git a/maps/feature-map/agents.md b/maps/feature-map/agents.md index 14291c099e0..78739494253 100644 --- a/maps/feature-map/agents.md +++ b/maps/feature-map/agents.md @@ -186,13 +186,13 @@ live VM provisioning. Manual: Hire an agent with a distinct name and role. Complete any required approval, reload the roster, and open its configuration. Confirm manager, adapter, and model persisted; run a small task to prove execution readiness. For a new Codex hire, choose the Codex harness. Automatic selection uses Paperclip -Runner on a qualified selected target; Advanced offers an explicit Legacy runner -override. Record the persisted runner and +Runner on a qualified selected target. Hiring and configuration do not expose a +runner dropdown; omit Advanced when no other settings remain. Record the persisted runner and underlying harness, rather than treating the displayed Codex name as execution proof. See [runtime selection and validation](./runs-adapters.md#adapter-setup) and [target qualification](./execution-environments.md#environment-settings). -Repeat with an explicit Legacy Codex hire. If approval is required, compare its +Repeat with an explicit Legacy Codex hire through the API or CLI. If approval is required, compare its reviewed execution configuration before and after restart and activation; approval must retain the reviewed runner. Test an existing Legacy and native agent through an unrelated edit, reload and restart: neither is automatically upgraded. @@ -207,8 +207,7 @@ Use the [portability recipe](./companies.md) for package preparation and boundar For Claude Code on qualified macOS or GNU/Linux x64/ARM64, submit without a runner override and inspect saved configuration: `paperclip_runner`, `provider: acpx`, `acpxAgent: claude`, -and ordinary model `claude-opus-5-5`. The Advanced runner selector can explicitly -choose Legacy. Keep explicit model/account choices and saved runners on edits. +and ordinary model `claude-opus-5-5`. Keep explicit model/account choices and saved runners on edits. Verify useful output and a follow-up after a controller restart; setup success alone does not establish session continuity. Unsupported architectures/libc and unknown environment qualification retain Legacy. Darwin SSH also defaults to Legacy until automatic matching assets are @@ -240,11 +239,11 @@ Check that it explains that verification is paused and offers **Retry now**. Fix retry, and reload; the failure must disappear only after readiness commits. Provider output and credentials must not appear in the lifecycle message. -An explicit runner change is a configuration change, with normal revisions and +An explicit runner change through the API or CLI is a configuration change, with normal revisions and session invalidation. Obsolete Claude tuning options (including maxTurnsPerRun) are ignored with a field-specific warning; permission, network, filesystem and command incompatibilities still produce an error. Selecting another runner must not silently discard security settings or change an active run's -recorded execution. Keep runner choice in Advanced and verify the next eligible +recorded execution. Verify the next eligible run separately from historical transcripts. Existing legacy Claude, Codex, and Grok Build agents on qualified default environments, or diff --git a/maps/feature-map/onboarding.md b/maps/feature-map/onboarding.md index eca80801046..dcd8c051707 100644 --- a/maps/feature-map/onboarding.md +++ b/maps/feature-map/onboarding.md @@ -67,9 +67,9 @@ company's onboarding route, and finish; drafts save automatically. Follow the created first task, verify the selected agent and company, and inspect its output or explicit authentication gate. Reload both company and task. -For Codex or Claude, choose the harness and leave runner selection automatic to prove the -first-agent default. On a qualified target it saves Paperclip Runner; Advanced -also permits Legacy. Do not switch runners after onboarding when claiming proof +For Codex or Claude, choose the harness to prove automatic first-agent runner +selection. On a qualified target it saves Paperclip Runner. The wizard has no +runner dropdown or empty Advanced section. Do not switch runners after onboarding when claiming proof of that default. Test the selected runtime and account, then complete a bounded useful task and follow-up with an independently checked file artifact. Saved execution configuration and recorded run identity must match after reload. @@ -108,7 +108,7 @@ uses the existing saved environment probe when authoritative qualification is absent. The board actor needs the existing instance-environment administration permission. Refresh company/environment-scoped discovery before testing the selected runtime. A denied probe, unconfirmed cleanup or setup failure must be -visible; selecting Legacy remains an explicit choice. Creation and discovery +visible and retryable through setup. Creation and discovery themselves allocate no target. A valid measured unsupported target follows the server's Legacy default. Named Cloud snapshots remain a separate unqualified boot-identity and fresh-signup gap. diff --git a/maps/feature-map/runs-adapters.md b/maps/feature-map/runs-adapters.md index 89636044630..014a56832a1 100644 --- a/maps/feature-map/runs-adapters.md +++ b/maps/feature-map/runs-adapters.md @@ -129,7 +129,8 @@ own login keys must remain unavailable. Verify the same delivery without an agent identity. Shell-policy metadata must contain names rather than values. Choose Codex as the harness; the UI displays its name/icon for both runners. -Advanced identifies the automatic default and offers Paperclip Runner or Legacy. +Setup selects the runner automatically, and edits preserve saved execution. +The UI has no runner dropdown; Advanced appears only when other settings remain. New Codex agents default to the native runner on supported macOS or GNU/Linux targets; the [shared selection contract](../../packages/shared/src/agent-runner.ts) and [runtime prerequisites](../../packages/paperclip-runner/README.md) define the diff --git a/server/src/services/agent-runner-selection.test.ts b/server/src/services/agent-runner-selection.test.ts index 5d877ccb949..85eaf1998c9 100644 --- a/server/src/services/agent-runner-selection.test.ts +++ b/server/src/services/agent-runner-selection.test.ts @@ -217,11 +217,11 @@ describe("server-owned Codex runner selection", () => { }); it.each(["minimal", "medium", "xhigh"])("keeps unsupported Pi thinking %s explicit", thinking => { const input = { adapterType: "pi_local", target, adapterConfig: { model: "custom/model", thinking } }; - expect(() => resolveNewAgentRunner(input)).toThrow("Legacy runner"); + expect(() => resolveNewAgentRunner(input)).toThrow("cannot honor"); expect(resolveNewAgentRunner({ ...input, runner: "legacy" })).toEqual({ adapterType: input.adapterType, adapterConfig: input.adapterConfig }); }); it.each([{ command: "custom-pi" }, { args: ["--extension", "custom.ts"] }, { extraArgs: "--no-tools" }, { stdoutLogMode: "raw" }, { thinking: "low", piThinkingLevel: "high" }])("rejects unsupported Pi configuration %j", extra => { - expect(() => resolveNewAgentRunner({ adapterType: "pi_local", target, adapterConfig: { model: "custom/model", ...extra } })).toThrow("Legacy runner"); + expect(() => resolveNewAgentRunner({ adapterType: "pi_local", target, adapterConfig: { model: "custom/model", ...extra } })).toThrow("cannot honor"); }); it.each([undefined, "", "model", "provider/"])("requires a Pi provider/model outside inactive setup: %s", model => { expect(() => resolveNewAgentRunner({ adapterType: "pi_local", target, adapterConfig: { model } })).toThrow("provider/model"); @@ -266,7 +266,7 @@ describe("server-owned Codex runner selection", () => { }); it.each([{ command: "custom-opencode" }, { variant: "high" }, { extraArgs: ["--agent", "plan"] }, { args: ["--attach", "http://localhost"] }, { dangerouslySkipPermissions: false }])("rejects unsupported OpenCode settings instead of discarding them: %j", adapterConfig => { const input = { adapterType: "opencode_local", adapterConfig: { model: "custom/model", ...adapterConfig }, target }; - expect(() => resolveNewAgentRunner(input)).toThrow("Legacy runner"); + expect(() => resolveNewAgentRunner(input)).toThrow("cannot honor"); expect(resolveNewAgentRunner({ ...input, runner: "legacy" })).toEqual({ adapterType: input.adapterType, adapterConfig: input.adapterConfig }); }); it.each([undefined, "", "model", "provider/"])("requires an explicit OpenCode provider/model: %s", model => { @@ -333,7 +333,7 @@ describe("server-owned Codex runner selection", () => { expect(() => resolveNewAgentRunner({ adapterType: "codex_local", target })).toThrow("Codex is disabled"); }); it.each(["extraArgs", "agentCommand", "networkScope", "filesystemScope", "search"])("rejects incompatible %s without changing runner", field => { - expect(() => resolveNewAgentRunner({ adapterType: "codex_local", target, adapterConfig: { [field]: true } })).toThrow("Legacy runner"); + expect(() => resolveNewAgentRunner({ adapterType: "codex_local", target, adapterConfig: { [field]: true } })).toThrow(field); try { resolveNewAgentRunner({ adapterType: "codex_local", target, adapterConfig: { [field]: true } }); } catch (error) { expect(error).toMatchObject({ status: 422, details: { fields: [field] } }); } }); @@ -492,7 +492,7 @@ describe("server-owned Codex runner selection", () => { it("rejects explicit Paperclip execution on an unqualified forced Kubernetes target without provisioning", async () => { state.settings = { general: { executionMode: "kubernetes" } }; await expect(resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", runner: "paperclip" })) - .rejects.toMatchObject({ status: 422, message: expect.stringContaining("Legacy runner"), details: { code: "agent_runner_unavailable" } }); + .rejects.toMatchObject({ status: 422, message: expect.stringContaining("environment connection probe"), details: { code: "agent_runner_unavailable" } }); expect(state.ensureKubernetesEnvironment).not.toHaveBeenCalled(); expect(state.resolveEnvironment).not.toHaveBeenCalled(); expect(state.sandboxQualification).not.toHaveBeenCalled(); @@ -848,7 +848,7 @@ describe("Claude runner defaults", () => { }); it("does not pin the ordinary model over a plain provider-routing binding", () => { expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, - adapterConfig: { env: { CLAUDE_CODE_USE_BEDROCK: { type: "plain", value: "1" } } } })).toThrow("Legacy runner"); + adapterConfig: { env: { CLAUDE_CODE_USE_BEDROCK: { type: "plain", value: "1" } } } })).toThrow("model"); }); it.each(["permissionMode", "acpPermissionMode"])("translates the legacy Claude %s without granting broader permissions", key => { for (const mode of ["deny-all", "approve-reads", "approve-all"]) { @@ -857,7 +857,7 @@ describe("Claude runner defaults", () => { expect(native.adapterConfig[key]).toBeUndefined(); } expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { [key]: "deny-all", acpxPermissionMode: "approve-all" } })).toThrow("cannot honor"); - expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { [key]: "unrecognized" } })).toThrow("Legacy runner"); + expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { [key]: "unrecognized" } })).toThrow(key); }); it("retains saved native Claude and allows an explicit compatible Legacy switch", () => { const saved = { adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "claude", model: "custom", acpxPermissionMode: "approve-all", lifecycleMode: "per_turn" } }; @@ -876,7 +876,7 @@ describe("Claude runner defaults", () => { } }); it.each(["agentCommand", "networkScope"])("rejects unsupported %s without fallback", key => { - expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { [key]: "configured" } })).toThrow("Legacy runner"); + expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { [key]: "configured" } })).toThrow(key); }); it.each(["low", "medium", "high", "xhigh", "max"])("translates Claude effort %s and preserves it on an explicit Legacy switch", effort => { const native = resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { effort } }); diff --git a/server/src/services/agent-runner-selection.ts b/server/src/services/agent-runner-selection.ts index fb902c668f5..b34873d6d07 100644 --- a/server/src/services/agent-runner-selection.ts +++ b/server/src/services/agent-runner-selection.ts @@ -81,7 +81,7 @@ export function agentRunnerAvailability(harness: string, target: RunnerTarget = } function incompatible(fields: string[]): never { - throw unprocessable(`Paperclip Runner cannot honor these settings: ${fields.join(", ")}. Remove them or select Legacy runner in Advanced.`, { code: "agent_runner_config_incompatible", fields }); + throw unprocessable(`Paperclip Runner cannot honor these settings: ${fields.join(", ")}. Remove them before retrying.`, { code: "agent_runner_config_incompatible", fields }); } function resolveGrokRunnerConfig(config: Record): Record { @@ -262,8 +262,8 @@ function resolveAgentRunner(input: AgentRunnerSelection): { adapterType: string; } if (native && harness !== "codex_local" && choice === "paperclip") return { adapterType, adapterConfig: normalizeLegacyRunnerProvider(config) }; if (!availability.supportedRunners.includes("paperclip")) throw unprocessable(input.target?.driver === "sandbox" - ? "Paperclip Runner is unavailable for this sandbox. Choose an immutable OCI image digest and run its environment connection probe to qualify the OS, architecture and libc, or select Legacy runner in Advanced." - : "Paperclip Runner is unavailable for this harness and environment. Select Legacy runner in Advanced.", { code: "agent_runner_unavailable" }); + ? "Paperclip Runner is unavailable for this sandbox. Choose an immutable OCI image digest and run its environment connection probe to qualify the OS, architecture and libc." + : "Paperclip Runner is unavailable for this harness and environment. Choose a supported harness or environment.", { code: "agent_runner_unavailable" }); if (native) return { adapterType, adapterConfig: normalizeLegacyRunnerProvider(config) }; if (harness === "cursor_cloud") { const unsupported = ["promptTemplate", "bootstrapPromptTemplate"].filter(key => typeof config[key] === "string" && config[key].trim()); @@ -451,7 +451,7 @@ export async function resolveAgentRunnerTargetForCompany(db: Db, companyId: stri if (parsed.driver !== "ssh") throw unprocessable("The selected SSH environment is unavailable."); let output: string; try { output = (await runSshCommand(parsed.config, AGENT_RUNNER_SSH_PLATFORM_COMMAND, { timeoutMs: 10_000 })).stdout; } - catch { throw unprocessable("Could not check the SSH host's platform. Check its connection and retry, or select Legacy runner in Advanced.", { code: "agent_runner_platform_probe_failed" }); } + catch { throw unprocessable("Could not check the SSH host's platform. Check its connection and retry.", { code: "agent_runner_platform_probe_failed" }); } const [os = "", arch = "", libc, glibcVersion] = output.trim().split(/\r?\n/); target = { driver, platform: os === "Linux" ? "linux" : os === "Darwin" ? "darwin" : "unknown", architecture: arch === "x86_64" ? "x64" : ["aarch64", "arm64"].includes(arch) ? "arm64" : arch, ...(os === "Linux" && libc !== undefined ? { libc: libc === "glibc" || libc === "musl" ? libc : "unknown", ...(libc === "glibc" && glibcVersion !== undefined ? { glibcVersion } : {}) } : {}) }; } diff --git a/server/src/services/heartbeat-runner-provider-config.test.ts b/server/src/services/heartbeat-runner-provider-config.test.ts index b209a0ff706..9bbb1f1323b 100644 --- a/server/src/services/heartbeat-runner-provider-config.test.ts +++ b/server/src/services/heartbeat-runner-provider-config.test.ts @@ -150,7 +150,7 @@ describe("Paperclip Runner native provider configuration", () => { expect(changedModel).not.toHaveProperty("modelReasoningEffort"); expect(resolvePaperclipRunnerNativeProviderInput({ backend: "acpx_runtime", adapterConfig: { ...base, chrome: true, maxTurnsPerRun: 1000 } })) .toEqual(resolvePaperclipRunnerNativeProviderInput({ backend: "acpx_runtime", adapterConfig: base })); - expect(() => resolvePaperclipRunnerNativeProviderInput({ backend: "acpx_runtime", adapterConfig: { ...base, networkScope: "unrestricted" } })).toThrow("Legacy runner"); + expect(() => resolvePaperclipRunnerNativeProviderInput({ backend: "acpx_runtime", adapterConfig: { ...base, networkScope: "unrestricted" } })).toThrow("networkScope"); }); it("projects Grok task model and effort while retaining the search policy and account", () => { diff --git a/server/src/services/native-runtime/provider-profile.ts b/server/src/services/native-runtime/provider-profile.ts index d1d7cb0bc93..f87eb6ab683 100644 --- a/server/src/services/native-runtime/provider-profile.ts +++ b/server/src/services/native-runtime/provider-profile.ts @@ -480,7 +480,7 @@ export function resolvePaperclipRunnerProviderProfile( if (candidate === "acpx" && agentHarnessType("paperclip_runner", config) === "claude_local") { const unsupported = claudeRunnerUnsupportedSettings(config); if (unsupported.length) throw new PaperclipRunnerProviderProfileError("agent_runner_config_incompatible", - `Paperclip Runner cannot honor these settings: ${unsupported.join(", ")}. Remove them or select Legacy runner in Advanced.`); + `Paperclip Runner cannot honor these settings: ${unsupported.join(", ")}. Remove them before retrying.`); } assertPermissionMode(candidate, config); try { diff --git a/server/src/services/native-runtime/setup-readiness.test.ts b/server/src/services/native-runtime/setup-readiness.test.ts index 139cdcde839..9f044c45f48 100644 --- a/server/src/services/native-runtime/setup-readiness.test.ts +++ b/server/src/services/native-runtime/setup-readiness.test.ts @@ -180,7 +180,7 @@ describe("Codex selected native account verification", () => { expect(nativeProbe).not.toHaveBeenCalled(); expect(sshExecute).not.toHaveBeenCalled(); expect(result.checks[0].message).toContain(source === "configured" ? "env.CODEX_HOME" : "operator CODEX_HOME"); - expect(result.checks[0].message).toContain("Legacy runner"); + expect(result.checks[0].message).not.toContain("Advanced"); expect(result.checks[0].message).toContain("managed Codex Connection"); } else expect(nativeProbe).toHaveBeenCalledOnce(); expect(await readFile(stored, "utf8")).toBe(auth); @@ -444,7 +444,7 @@ describe("Codex selected native account verification", () => { it("keeps the generic hint when the backend rejects a model that is not retired", async () => { nativeProbe.mockRejectedValue(new Error("The 'gpt-6.1-sol' model is not supported when using Codex with a ChatGPT account.")); const result = await testNativeRunnerAuthentication(context, "codex", "gpt-6.1-sol"); - expect(result.checks[0].hint).toBe("Check the selected account, model access, and native runtime prerequisites, then retry. Legacy runner is available explicitly in Advanced."); + expect(result.checks[0].hint).toBe("Check the selected account, model access, and native runtime prerequisites, then retry."); }); it.each([ ["authentication required: selected-account", true], diff --git a/server/src/services/native-runtime/setup-readiness.ts b/server/src/services/native-runtime/setup-readiness.ts index 2e898442853..b20cfe7cf42 100644 --- a/server/src/services/native-runtime/setup-readiness.ts +++ b/server/src/services/native-runtime/setup-readiness.ts @@ -217,7 +217,7 @@ export async function testNativeRunnerAuthentication(context: AdapterEnvironment linked = (await lstat(authPath)).isSymbolicLink(); if (linked && !context.managedAiCredentialHome?.trim()) { const field = environment.CODEX_HOME?.trim() ? "env.CODEX_HOME" : "operator CODEX_HOME"; - throw new Error(`${field} uses a linked auth.json that Paperclip Runner tasks cannot read. Choose Legacy runner in Advanced or use a managed Codex Connection.`); + throw new Error(`${field} uses a linked auth.json that Paperclip Runner tasks cannot read. Use a managed Codex Connection.`); } const resolvedAuthPath = await realpath(authPath); const source = await stat(resolvedAuthPath); @@ -260,7 +260,7 @@ export async function testNativeRunnerAuthentication(context: AdapterEnvironment const rejectedModel = provider === "codex" ? CODEX_CHATGPT_MODEL_REJECTION_RE.exec(message)?.[1] : undefined; const retirementHint = rejectedModel ? codexChatGptRetirementHint(rejectedModel) : null; return { adapterType: "paperclip_runner", status: "fail", testedAt: new Date().toISOString(), checks: [{ code: `${provider}_hello_probe_${authentication ? "auth_required" : /timed out/i.test(message) ? "timeout" : "failed"}`, level: "error", message, - hint: retirementHint ?? "Check the selected account, model access, and native runtime prerequisites, then retry. Legacy runner is available explicitly in Advanced." }, + hint: retirementHint ?? "Check the selected account, model access, and native runtime prerequisites, then retry." }, ...(authentication && provider === "codex" ? [{ code: ADAPTER_AUTH_MISSING_CHECK_CODE, level: "error" as const, message: "The selected Codex account needs authentication.", hint: "Sign in to Codex or choose an authenticated account, then test again." }] : [])] }; diff --git a/tests/runner-e2e/README.md b/tests/runner-e2e/README.md index 2cdca967e73..f6be16f3c1d 100644 --- a/tests/runner-e2e/README.md +++ b/tests/runner-e2e/README.md @@ -1824,9 +1824,12 @@ There are thirteen cases on `legacy-codex`, `legacy-claude`, `runner-codex`, and production wizard, omits the runner override, and verifies the saved native configuration without changing runners afterward. `runner-acpx-claude` does the same for Claude on its qualified default targets and inspects the saved ACPX -Claude profile directly. Both Legacy profiles select Legacy runner in Advanced -before authentication and creation. All four paths are labeled -`production-wizard`; there is no post-onboarding runner switch. Persona, +Claude profile directly. Both Legacy profiles configure the inactive setup record +through the normal agent API before authentication, reload, and finish the saved +setup in the wizard. They are labeled `api-configured-legacy-setup`; the native +paths retain `production-wizard`. The UI exposes no runner dropdown, and there +is no post-onboarding runner switch. Legacy coverage proves setup resumption, +not the fresh-agent automatic default. Persona, managed instructions, skills, seeded question, and task invocation are preserved. The native path removes the legacy Paperclip operational skill because Runner supplies that contract through its protocol; other assigned skills, including `/first-task`, are retained. diff --git a/tests/runner-e2e/first-task-flow.ts b/tests/runner-e2e/first-task-flow.ts index d1201dae5f7..b16e115fc93 100644 --- a/tests/runner-e2e/first-task-flow.ts +++ b/tests/runner-e2e/first-task-flow.ts @@ -287,6 +287,7 @@ export async function setupFirstTaskFixtures(input: { onCompanyCreated?: (companyId: string) => Promise; }): Promise { const { page, api, execution, nonce } = input; + const legacySetup = ["legacy-codex", "legacy-claude"].includes(execution.profile.id); await api.patch("/api/instance/settings/experimental", { enableClassicTaskInterface: false, }); @@ -333,16 +334,27 @@ export async function setupFirstTaskFixtures(input: { : /^Claude/, }) .click(); + if (legacySetup) { + const agents = await pollUntil({ + label: "first-task agent setup configuration", + deadlineAt: Date.now() + 30_000, + load: () => api.get(`/api/companies/${company.id}/agents`), + accept: rows => rows.some(agent => agent.name === fixtures.agent.name + && agent.pauseReason === "setup" && agent.metadata?.setupAdapterPending === false), + }); + const agent = agents.find(agent => agent.name === fixtures.agent.name && agent.pauseReason === "setup")!; + await api.patch(`/api/agents/${agent.id}`, { runner: "legacy" }); + await page.reload({ waitUntil: "domcontentloaded" }); + await page.getByRole("radio", { + name: execution.profile.credential === "OPENAI_API_KEY" ? /^OpenAI/ : /^Claude/, + }).click(); + } + await expect(page.getByRole("button", { name: "Runner", exact: true })).toHaveCount(0); const useKey = page.getByRole("button", { name: "Use API key instead", exact: true, }); const savedKey = page.getByRole("combobox", { name: "Saved API key" }); - if (["legacy-codex", "legacy-claude"].includes(execution.profile.id)) { - await page.getByText("Advanced", { exact: true }).click(); - await page.getByRole("button", { name: "Runner", exact: true }).click(); - await page.getByRole("option", { name: "Legacy runner", exact: true }).click(); - } // Credential mode depends on the selected provider and its asynchronous key lookup. await expect(savedKey.or(useKey).first()).toBeVisible(); if (await useKey.isVisible()) await useKey.click(); @@ -374,7 +386,7 @@ export async function setupFirstTaskFixtures(input: { expect(agents[0].adapterConfig?.provider).toBe("codex"); } fixtures.onboardingRuntime = { - mode: "production-wizard", + mode: legacySetup ? "api-configured-legacy-setup" : "production-wizard", originalAdapterType: wizardAdapter, testedAdapterType: wizardAdapter, originalModel: agents[0].adapterConfig?.model ?? null, diff --git a/tests/runner-e2e/live-fixtures.ts b/tests/runner-e2e/live-fixtures.ts index d1a71cbe45f..306da7fe9b1 100644 --- a/tests/runner-e2e/live-fixtures.ts +++ b/tests/runner-e2e/live-fixtures.ts @@ -75,7 +75,7 @@ export interface LiveFixtureValues { setupQualification?: { recorded: boolean; adapterType: string | null; platformResponses: number; adapterResponses: number }; onboardingRuntime?: { - mode: "production-wizard" | "post-onboarding-runtime-switch"; + mode: "production-wizard" | "api-configured-legacy-setup" | "post-onboarding-runtime-switch"; originalAdapterType: string; originalModel: string | null; testedAdapterType: string; diff --git a/ui/src/components/AgentConfigForm.render.test.tsx b/ui/src/components/AgentConfigForm.render.test.tsx index d6fca41489c..08e11823def 100644 --- a/ui/src/components/AgentConfigForm.render.test.tsx +++ b/ui/src/components/AgentConfigForm.render.test.tsx @@ -651,6 +651,8 @@ describe("AgentConfigForm environment selector", () => { const values = { ...defaultCreateValues, adapterType: "claude_local", runner: "auto" as const }; const result = await renderCreateForm([], values); roots.push(result.root); + expect(result.container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect(result.container.querySelector('[data-config-section="configuration"]')?.textContent?.includes("Advanced")).toBe(defaultRunner === "legacy"); const resolved = result.onChange.mock.calls.reduce((current, [patch]) => ({ ...current, ...patch }), values); expect(resolved.resolvedRunner).toBe(defaultRunner); const config = buildClaudeLocalConfig(resolved); @@ -714,8 +716,13 @@ describe("AgentConfigForm environment selector", () => { const result = await renderForm([], { adapterType, adapterConfig }, { showAdapterTypeField: true }); roots.push(result.root); expect(result.container.querySelector('[aria-label="Harness"]')?.textContent).toContain("Codex"); - await clickByText(result.container, "Advanced"); - expect(result.container.querySelector('[aria-label="Runner"]')?.textContent).toContain(adapterType === "paperclip_runner" ? "Paperclip Runner" : "Legacy runner"); + expect(result.container.querySelector('[aria-label="Runner"]')).toBeNull(); + if (adapterType === "codex_local") { + await clickByText(result.container, "Advanced"); + const labels = [...result.container.querySelectorAll("label")].map(label => label.textContent?.trim()); + expect(labels).toContain("Command"); + expect(labels).toContain("Extra args (comma-separated)"); + } await act(async () => setInputValue(result.container.querySelector('[placeholder="Agent name"]')!, "Renamed Cody")); await flushReact(); await clickByText(result.container, "Save"); @@ -842,8 +849,8 @@ describe("AgentConfigForm environment selector", () => { try { const result = await renderForm([], { adapterType: "claude_local", adapterConfig: { model: "claude-opus-5-5" } }); roots.push(result.root); - await clickByText(result.container, "Advanced"); - expect(result.container.querySelector('[aria-label="Runner"]')?.textContent).toContain("Paperclip Runner"); + expect(result.container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect(result.container.querySelector('[data-config-section="configuration"]')?.textContent).not.toContain("Advanced"); await clickByText(result.container, "Save"); expect(result.onSave).toHaveBeenCalledWith(expect.objectContaining({ runner: "paperclip" })); } finally { mockAdaptersApi.list.mockResolvedValue([{ type: "codex_local", supportedRunners: ["legacy", "paperclip"], defaultRunner: "legacy" }]); } @@ -1018,18 +1025,15 @@ describe("AgentConfigForm environment selector", () => { makeEnvironment({ id: "sandbox-2", name: "Daytona Two", driver: "sandbox", config: { provider: "daytona" } }), ], { adapterType, adapterConfig, defaultEnvironmentId: "sandbox-1" }, { showAdapterTypeField: true }); roots.push(result.root); - await clickByText(result.container, "Advanced"); - const runnerLabel = adapterType === "paperclip_runner" ? "Paperclip Runner" : "Legacy runner"; - await selectEnvironment(result.container, "sandbox-2"); expect(result.container.querySelector('[aria-label="Environment override"]')?.textContent).toContain("Daytona Two"); - expect(result.container.querySelector('[aria-label="Runner"]')?.textContent).toContain(runnerLabel); + expect(result.container.querySelector('[aria-label="Runner"]')).toBeNull(); await clickByText(result.container, "Save"); expect(result.onSave).toHaveBeenLastCalledWith({ defaultEnvironmentId: "sandbox-2" }); await selectEnvironment(result.container, ""); expect(result.container.querySelector('[aria-label="Environment override"]')?.textContent).toContain("Default: Local"); - expect(result.container.querySelector('[aria-label="Runner"]')?.textContent).toContain(runnerLabel); + expect(result.container.querySelector('[aria-label="Runner"]')).toBeNull(); await clickByText(result.container, "Save"); expect(result.onSave).toHaveBeenLastCalledWith({ defaultEnvironmentId: null }); }); @@ -1041,14 +1045,13 @@ describe("AgentConfigForm environment selector", () => { expect(result.onChange).toHaveBeenLastCalledWith({ defaultEnvironmentId: "" }); }); - it("submits only an explicit runner change and retains saved configuration for server translation", async () => { - const result = await renderForm([], { adapterType: "paperclip_runner", adapterConfig: { provider: "codex", model: "gpt-5.6-sol", reasoningEffort: "high", env: { CODEX_HOME: { type: "secret_ref", secretId: "codex-account" } } } }); + it.each(["codex", "claude"])("hides empty Advanced settings for the saved native %s harness", async harness => { + const profile = harness === "codex" ? { provider: "codex" } : { provider: "acpx", acpxAgent: "claude" }; + const result = await renderForm([], { adapterType: "paperclip_runner", adapterConfig: { ...profile, model: "saved-model" } }); roots.push(result.root); - await clickByText(result.container, "Advanced"); - await clickElement(result.container.querySelector('[aria-label="Runner"]')); - await clickElement([...document.querySelectorAll('[role="option"]')].find(option => option.textContent?.startsWith("Legacy runner"))); - await clickByText(result.container, "Save"); - expect(result.onSave).toHaveBeenCalledWith({ runner: "legacy" }); + const configuration = result.container.querySelector('[data-config-section="configuration"]')!; + expect(configuration.textContent).not.toContain("Advanced"); + expect(result.container.querySelector('[aria-label="Runner"]')).toBeNull(); }); it.each([false, true])("keeps experimental native selection gated when enableNativeRunner is %s", async enabled => { @@ -1069,13 +1072,13 @@ describe("AgentConfigForm environment selector", () => { expect(result.onSave.mock.calls[0][0].runner).toBeUndefined(); }); - it("shows Claude's saved runner without legacy command controls when the experimental gate is off", async () => { + it("preserves Claude's saved runner without runner or legacy command controls when the experimental gate is off", async () => { mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableNativeRunner: false }); const result = await renderForm([], { adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "claude", model: "claude-opus-5-5", modelReasoningEffort: "high" } }, { showAdapterTypeField: true }); roots.push(result.root); expect(result.container.querySelector('[aria-label="Harness"]')?.textContent).toContain("Claude"); - await clickByText(result.container, "Advanced"); - expect(result.container.querySelector('[aria-label="Runner"]')?.textContent).toContain("Paperclip Runner"); + expect(result.container.querySelector('[data-config-section="configuration"]')?.textContent).not.toContain("Advanced"); + expect(result.container.querySelector('[aria-label="Runner"]')).toBeNull(); const labels = Array.from(result.container.querySelectorAll("label")).map(label => label.textContent?.trim()); expect(labels).not.toContain("Command"); expect(labels).not.toContain("Extra args (comma-separated)"); @@ -1091,9 +1094,10 @@ describe("AgentConfigForm environment selector", () => { roots.push(result.root); if (adapterConfig.provider === "opencode" || adapterConfig.acpxAgent === "pi") { expect(result.container.querySelector('[aria-label="Harness"]')?.textContent).toContain(adapterConfig.provider === "opencode" ? "OpenCode" : "Pi"); - await clickByText(result.container, "Advanced"); - expect(result.container.querySelector('[aria-label="Runner"]')?.textContent).toContain("Paperclip Runner"); - } else expect(result.container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect(result.container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect(result.container.querySelector('[data-config-section="configuration"]')?.textContent).not.toContain("Advanced"); + } + expect(result.container.querySelector('[aria-label="Runner"]')).toBeNull(); if (adapterConfig.acpxAgent === "pi") { expect(result.container.querySelector('[aria-label="Pi thinking level"]')).not.toBeNull(); expect(result.container.querySelector('[aria-label="Thinking effort"]')).toBeNull(); diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 6ede0845965..5f8eb70185c 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -3,7 +3,6 @@ import { useConnectionModels } from "./ai-connections/useConnectionModels"; import { agentHarnessType, agentRunner, paperclipRunnerProfileForHarness, normalizeClaudeRunnerOptions, type AgentRunnerChoice } from "@paperclipai/shared"; import { adaptersApi } from "../api/adapters"; import { canQualifyRunnerEnvironment, qualifyRunnerEnvironment } from "../lib/qualify-claude-setup"; -import { AgentRunnerSelect } from "./AgentRunnerSelect"; import { aiRoutingHarness } from "@paperclipai/shared"; import { AiConnectionField } from "./ai-connections/AiConnectionField"; import { aiConnectionBindingSchema, aiRuntimeConnectionBindingSchema } from "@paperclipai/shared"; @@ -1125,6 +1124,11 @@ export function AgentConfigForm(props: AgentConfigFormProps) { const [runPolicyAdvancedOpen, setRunPolicyAdvancedOpen] = useState(false); const [configurationAdvancedOpen, setConfigurationAdvancedOpen] = useState(false); const configSchema = useConfigSchema(adapterType); + const hasLocalAdvancedFields = isLocal && (!isRunnerHarness || fieldsAdapterType !== "paperclip_runner"); + const hasConfigurationAdvancedFields = hasLocalAdvancedFields + || adapterType === "process" + || (adapterType === "claude_local" && !hideHostPaths && (isCreate ? props.values.claudeEngine : eff("adapterConfig", "engine", config.engine)) === "acp") + || (fieldsAdapterType !== "paperclip_runner" && configSchema?.fields.some((field) => schemaFieldSection(field.key) === "advanced")); const renderAdapterFields = (section: AdapterConfigSection) => ( <> {adapterType === "claude_local" && } @@ -1887,6 +1891,11 @@ export function AgentConfigForm(props: AgentConfigFormProps) { )} + {isRunnerHarness && runnerDiscoveryView.kind === "error" && ( + + )} {!isExternalRunner && !isCreate && selectedCompanyId && | undefined)?.aiConnection ?? runtimeConfig.aiConnection).data} model={String(eff("adapterConfig", "model", config.model) ?? "")} environmentId={currentDefaultEnvironmentId || undefined} legacy @@ -2081,19 +2090,14 @@ export function AgentConfigForm(props: AgentConfigFormProps) { )} {renderAdapterFields("configuration")} - {(isLocal || adapterType === "process" || configSchema?.fields.some((field) => schemaFieldSection(field.key) === "advanced")) && ( + {hasConfigurationAdvancedFields && ( setConfigurationAdvancedOpen(!configurationAdvancedOpen)} >
- {isRunnerHarness && { void runnerDiscovery.refetch(); }} - defaultRunner={runnerAvailability?.defaultRunner} - supportedRunners={runnerAvailability?.supportedRunners} - onChange={next => isCreate ? set!({ runner: next }) : setOverlay(prev => ({ ...prev, runner: next }))} />} - {isLocal && (!isRunnerHarness || fieldsAdapterType !== "paperclip_runner") && (<> {/* + {hasLocalAdvancedFields && (<> {/* The command names a binary on the execution host, so the managed-sandbox-only policy hides it: the platform-managed image owns the binary. Hiding is presentation only. A stored diff --git a/ui/src/components/AgentRunnerSelect.tsx b/ui/src/components/AgentRunnerSelect.tsx deleted file mode 100644 index f2f72e2d6ff..00000000000 --- a/ui/src/components/AgentRunnerSelect.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { AgentRunner, AgentRunnerChoice } from "@paperclipai/shared"; -import { Field } from "./agent-config-primitives"; -import { SelectPopover } from "./ui/select"; -import { Button } from "./ui/button"; - -export function AgentRunnerSelect({ - value, - onChange, - defaultRunner, - supportedRunners = ["legacy"], - disabled, - pending, - error, - onRetry, -}: { - value: AgentRunnerChoice; - onChange: (value: AgentRunnerChoice) => void; - defaultRunner?: AgentRunner; - supportedRunners?: readonly AgentRunner[]; - disabled?: boolean; - pending?: boolean; - error?: string; - onRetry?: () => void; -}) { - return ( - - onChange(next as AgentRunnerChoice)} - options={[ - { value: "paperclip", label: `Paperclip Runner${defaultRunner === "paperclip" ? " (default)" : ""}`, disabled: !supportedRunners.includes("paperclip") && value !== "paperclip" }, - { value: "legacy", label: `Legacy runner${defaultRunner === "legacy" ? " (default)" : ""}`, disabled: !supportedRunners.includes("legacy") && value !== "legacy" }, - ]} - /> - {error &&
-

{error}

- {onRetry && } -
} -
- ); -} diff --git a/ui/src/components/ConfigureBuiltInAgentModal.test.tsx b/ui/src/components/ConfigureBuiltInAgentModal.test.tsx index 4bc3e2d30c4..337844a3762 100644 --- a/ui/src/components/ConfigureBuiltInAgentModal.test.tsx +++ b/ui/src/components/ConfigureBuiltInAgentModal.test.tsx @@ -11,9 +11,6 @@ import type { BuiltInAgentState } from "@/api/builtInAgents"; const provisionMock = vi.hoisted(() => vi.fn()); const updateMock = vi.hoisted(() => vi.fn()); const adapterModelsMock = vi.hoisted(() => vi.fn()); -const adapterDiscoveryMock = vi.hoisted(() => vi.fn()); - -vi.mock("@/api/adapters", () => ({ adaptersApi: { list: adapterDiscoveryMock } })); vi.mock("@/api/builtInAgents", async (importOriginal) => { const actual = await importOriginal(); @@ -129,7 +126,6 @@ describe("ConfigureBuiltInAgentModal (PAP-12978)", () => { provisionMock.mockReset(); updateMock.mockReset(); adapterModelsMock.mockReset().mockResolvedValue([]); - adapterDiscoveryMock.mockReset().mockResolvedValue([{ type: "codex_local", supportedRunners: ["paperclip", "legacy"], defaultRunner: "legacy" }]); onOpenChange.mockReset(); onConfigured.mockReset(); }); @@ -207,21 +203,16 @@ describe("ConfigureBuiltInAgentModal (PAP-12978)", () => { ["paperclip_runner", "ready"], ["codex_local", "paused"], ["paperclip_runner", "paused"], - ] as const)("lets the server translate saved %s settings for a runner-only change while %s", async (adapterType, status) => { + ] as const)("preserves saved %s settings without offering runner selection while %s", async (adapterType, status) => { const savedConfig = { model: "gpt-5", timeoutSec: 45, modelReasoningEffort: "high", env: { CODEX_HOME: { type: "secret_ref", secretId: "account-1" } }, ...(adapterType === "paperclip_runner" ? { provider: "codex", codexPermissionMode: "never", lifecycleMode: "per_turn" } : {}) }; const state = makeState({ status, agentId: "a1", agent: makeSavedAgent(adapterType, savedConfig) }); provisionMock.mockResolvedValue(state); await renderModal(state); - const summary = document.querySelector("summary")!; - flushSync(() => summary.dispatchEvent(new MouseEvent("click", { bubbles: true }))); - flushSync(() => document.querySelector('[aria-label="Runner"]')!.dispatchEvent(new MouseEvent("click", { bubbles: true }))); - await flushReact(); - const runner = adapterType === "paperclip_runner" ? "legacy" : "paperclip"; - flushSync(() => document.querySelector(`[role="option"][data-value="${runner}"]`)!.dispatchEvent(new MouseEvent("click", { bubbles: true }))); - await flushReact(); + expect(document.querySelector('[aria-label="Runner"]')).toBeNull(); + expect(document.querySelector("summary")).toBeNull(); flushSync(() => findButton("Configure")!.dispatchEvent(new MouseEvent("click", { bubbles: true }))); await flushReact(); - expect(provisionMock).toHaveBeenCalledWith("c1", "briefs", { runner }); + expect(provisionMock).toHaveBeenCalledWith("c1", "briefs", {}); expect(state.agent?.adapterConfig).toEqual(savedConfig); }); diff --git a/ui/src/components/ConfigureBuiltInAgentModal.tsx b/ui/src/components/ConfigureBuiltInAgentModal.tsx index 4c197de9980..854fa1647d3 100644 --- a/ui/src/components/ConfigureBuiltInAgentModal.tsx +++ b/ui/src/components/ConfigureBuiltInAgentModal.tsx @@ -1,8 +1,6 @@ import { describeError } from "@/api/errors"; import { useMemo, useState } from "react"; -import { agentHarnessType, agentRunner, type AgentRunnerChoice } from "@paperclipai/shared"; -import { adaptersApi } from "@/api/adapters"; -import { AgentRunnerSelect } from "./AgentRunnerSelect"; +import { agentHarnessType } from "@paperclipai/shared"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { @@ -71,10 +69,6 @@ export function ConfigureBuiltInAgentModal({ const [adapterType, setAdapterType] = useState( () => state.agent && ["codex_local", "claude_local", "opencode_local", "pi_local"].includes(agentHarnessType(state.agent.adapterType, state.agent.adapterConfig)) ? agentHarnessType(state.agent.adapterType, state.agent.adapterConfig) : state.agent?.adapterType ?? defaultAdapterType(state), ); - const [runner, setRunner] = useState(); - const environmentId = state.agent?.defaultEnvironmentId ?? null; - const adapters = useQuery({ queryKey: queryKeys.adapters.availability(companyId, environmentId), queryFn: () => adaptersApi.list({ companyId, environmentId }), enabled: open }); - const runnerAvailability = adapters.data?.find(item => item.type === adapterType); const [model, setModel] = useState(() => { const config = state.agent?.adapterConfig; const configuredModel = typeof config === "object" && config !== null @@ -144,7 +138,6 @@ export function ConfigureBuiltInAgentModal({ : { adapterType, adapterConfig }; const result = await builtInAgentsApi.provision(companyId, definition.key, { ...adapterInput, - ...(["codex_local", "claude_local", "opencode_local", "pi_local"].includes(adapterType) && runner !== undefined ? { runner } : {}), ...(budgetMonthlyCents !== undefined ? { budgetMonthlyCents } : {}), }); return result; @@ -184,20 +177,11 @@ export function ConfigureBuiltInAgentModal({ onChange={(next) => { setAdapterType(next); setModel(""); - setRunner(undefined); }} disabledTypes={disabledTypes} /> - {["codex_local", "claude_local", "opencode_local", "pi_local"].includes(adapterType) &&
- Advanced - { void adapters.refetch(); }} - defaultRunner={runnerAvailability?.defaultRunner} supportedRunners={runnerAvailability?.supportedRunners} - onChange={setRunner} /> -
} - {modelRequired && ( // ModelDropdown supplies its own "Model" Field label + hint. [...document.body.querySelectorAll("button")].find(entry => entry.textContent?.trim() === "Use a custom gateway"); expect(gateway(), "the custom gateway control should still exist").toBeTruthy(); + expect(document.body.querySelector('[aria-label="Runner"]')).toBeNull(); const advanced = gateway()!.closest("details"); expect(advanced, "Use a custom gateway should live inside the Advanced disclosure").toBeTruthy(); expect(advanced!.querySelector(":scope > summary")?.textContent?.trim()).toBe("Advanced"); @@ -4155,6 +4156,8 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", ( })); await pickSource(source); await settle(); + expect(document.body.querySelector('[aria-label="Runner"]')).toBeNull(); + expect([...document.body.querySelectorAll("summary")].some(summary => summary.textContent === "Advanced")).toBe(false); expect(button("Connect").disabled).toBe(true); await act(async () => button("Connect").dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", metaKey: true, bubbles: true }))); await settle(); diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 7d23cc1ed2c..f65c9fad251 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -4,7 +4,6 @@ import { needsClaudeSetupQualification, qualifyClaudeSetup } from "../lib/qualif import { runOnboardingConnectionTest } from "../lib/onboarding-connection-test"; import { agentHarnessType } from "@paperclipai/shared"; import { adaptersApi, type AdapterInfo } from "../api/adapters"; -import { AgentRunnerSelect } from "./AgentRunnerSelect"; import { healthApi } from "@/api/health"; import { describeError } from "@/api/errors"; import { LocalProviderLoginInstructions } from "./AdapterLoginChrome"; @@ -2202,7 +2201,7 @@ function OnboardingWizardInner({ || typeof probe.details.leaseId !== "string" || !probe.details.leaseId) { throw new Error(typeof probe.details?.platformQualificationDiagnostic === "string" ? probe.details.platformQualificationDiagnostic - : probe.summary || "The environment's platform qualification could not be confirmed. Retry the connection probe or select Legacy runner in Advanced."); + : probe.summary || "The environment's platform qualification could not be confirmed. Retry the connection probe."); } const refreshed = await queryClient.fetchQuery({ queryKey: availabilityKey, staleTime: 0, queryFn: () => adaptersApi.list({ companyId: createdCompanyId, environmentId }, { signal }) }); @@ -3232,30 +3231,21 @@ function OnboardingWizardInner({ ) : null} - {sourceSelected && ["codex_local", "claude_local", "opencode_local", "pi_local"].includes(adapterType) &&
- Advanced - {/* The gateway is an alternative route, not the next - thing after the key, so it waits here rather than - sitting under the API key field (N-16). Same gates as - the key card it used to live in. */} - {connectCardLive && connectCardMounted && !connectProgress && credentialMode === "api" && createdCompanyId && (adapterType === "codex_local" || adapterType === "claude_local") && { - setGatewayConnection({ companyId: createdCompanyId, adapterType, binding }); - setApiKey(""); - setModel(""); - setError(null); - }} />} - { void runnerDiscovery.refetch(); }} - defaultRunner={runnerDiscovery.data?.find(item => item.type === adapterType)?.defaultRunner} - supportedRunners={runnerDiscovery.data?.find(item => item.type === adapterType)?.supportedRunners} - onChange={setRunner} /> -
} + {sourceSelected && connectCardLive && connectCardMounted && !connectProgress && credentialMode === "api" && createdCompanyId && (adapterType === "codex_local" || adapterType === "claude_local") && ( +
+ Advanced + { + setGatewayConnection({ companyId: createdCompanyId, adapterType, binding }); + setApiKey(""); + setModel(""); + setError(null); + }} /> +
+ )} {/* Conditional adapter fields */} diff --git a/ui/src/components/new-agent/NewAgentSetup.tsx b/ui/src/components/new-agent/NewAgentSetup.tsx index 8fc7cfd5856..1cbc123e747 100644 --- a/ui/src/components/new-agent/NewAgentSetup.tsx +++ b/ui/src/components/new-agent/NewAgentSetup.tsx @@ -7,7 +7,6 @@ import { useAgentAppearanceDraft } from "../../hooks/useAgentAppearanceDraft"; import { AiConnectionField, aiProviderForAdapter } from "../ai-connections/AiConnectionField"; import type { AiConnectionBinding } from "@paperclipai/shared"; import type { AgentRunner, AgentRunnerChoice } from "@paperclipai/shared"; -import { AgentRunnerSelect } from "../AgentRunnerSelect"; import { SelectPopover } from "../ui/select"; import { DEFAULT_CODEX_LOCAL_MODEL } from "@paperclipai/adapter-codex-local"; import { @@ -1118,14 +1117,6 @@ function Setup({ }).catch(cause => setError(cause instanceof Error ? cause.message : "Could not save agent setup.")); }} /> :

Loading connection settings…

} - {["codex_local", "claude_local", "opencode_local", "pi_local"].includes(adapterType) &&
- Advanced - { void adapters.refetch(); }} - defaultRunner={adapters.data?.find(item => item.type === adapterType)?.defaultRunner} - supportedRunners={adapters.data?.find(item => item.type === adapterType)?.supportedRunners} - onChange={value => { setRunner(value); resetTest(); }} /> -
} ) : screen === "saved" && created ? (
@@ -1478,14 +1469,6 @@ function Setup({
)} - {["codex_local", "claude_local", "opencode_local", "pi_local", "cursor_cloud"].includes(adapterType) &&
- Advanced - { void adapters.refetch(); }} - defaultRunner={adapters.data?.find(item => item.type === adapterType)?.defaultRunner} - supportedRunners={adapters.data?.find(item => item.type === adapterType)?.supportedRunners} - onChange={value => { setRunner(value); resetTest(); }} /> -
} {adapterType !== "hermes_gateway" && adapterType !== "cursor_cloud" && (
diff --git a/ui/src/lib/qualify-claude-setup.ts b/ui/src/lib/qualify-claude-setup.ts index 801025c07c3..dc7ad8ff504 100644 --- a/ui/src/lib/qualify-claude-setup.ts +++ b/ui/src/lib/qualify-claude-setup.ts @@ -54,7 +54,7 @@ export async function qualifyRunnerEnvironment(input: { || typeof probe.details.leaseId !== "string" || !probe.details.leaseId) { throw new Error(typeof probe.details?.platformQualificationDiagnostic === "string" ? probe.details.platformQualificationDiagnostic - : probe.summary || "The environment's platform qualification could not be confirmed. Retry the connection probe or select Legacy runner in Advanced."); + : probe.summary || "The environment's platform qualification could not be confirmed. Retry the connection probe."); } adapters = await input.readAdapters(); if (!input.isCurrent()) return undefined; diff --git a/ui/src/pages/CompanyImport.test.tsx b/ui/src/pages/CompanyImport.test.tsx index be7a80b5e78..ef09cd5e51a 100644 --- a/ui/src/pages/CompanyImport.test.tsx +++ b/ui/src/pages/CompanyImport.test.tsx @@ -1248,22 +1248,22 @@ describe("CompanyImport", () => { }); it.each([ - { sourceType: "codex_local", sourceRunner: "legacy" as const, runner: "paperclip" as const, sourcePolicy: {}, expectedBypass: undefined }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: false }, expectedBypass: false }, - { sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, runner: "legacy" as const, sourcePolicy: { codexPermissionMode: "never" }, expectedBypass: true }, - { sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, runner: "legacy" as const, sourcePolicy: {}, expectedBypass: true }, - { sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, runner: "legacy" as const, sourcePolicy: { codexPermissionMode: "never", dangerouslyBypassApprovalsAndSandbox: false }, expectedBypass: false }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassSandbox: false }, expectedBypass: false }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: true }, expectedBypass: true }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { extraArgs: ["--sandbox", "read-only"] }, expectedBypass: false }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { args: ["-c", "approval_policy=on-request"] }, expectedBypass: false }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { extraArgs: [], args: ["--profile", "restricted"] }, expectedBypass: false }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { extraArgs: ["--color", "never"], args: ["--sandbox", "read-only"] }, expectedBypass: true }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { extraArgs: ["-c", "sandbox_workspace_write.network_access=false"] }, expectedBypass: false }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: true, extraArgs: ["--sandbox", "read-only"] }, expectedBypass: true }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassSandbox: true, args: ["--sandbox", "read-only"] }, expectedBypass: true }, - { sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: false, dangerouslyBypassSandbox: true, extraArgs: ["--color", "never"] }, expectedBypass: false }, - ])("submits $runner from $sourceType with visible bypass=$expectedBypass while preserving saved policy", async ({ sourceType, sourceRunner, runner, sourcePolicy, expectedBypass }) => { + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: {}, expectedBypass: true }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: false }, expectedBypass: false }, + { sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, sourcePolicy: { codexPermissionMode: "never" }, expectedBypass: true }, + { sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, sourcePolicy: {}, expectedBypass: true }, + { sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, sourcePolicy: { codexPermissionMode: "never", dangerouslyBypassApprovalsAndSandbox: false }, expectedBypass: false }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { dangerouslyBypassSandbox: false }, expectedBypass: false }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: true }, expectedBypass: true }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { extraArgs: ["--sandbox", "read-only"] }, expectedBypass: false }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { args: ["-c", "approval_policy=on-request"] }, expectedBypass: false }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { extraArgs: [], args: ["--profile", "restricted"] }, expectedBypass: false }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { extraArgs: ["--color", "never"], args: ["--sandbox", "read-only"] }, expectedBypass: true }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { extraArgs: ["-c", "sandbox_workspace_write.network_access=false"] }, expectedBypass: false }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: true, extraArgs: ["--sandbox", "read-only"] }, expectedBypass: true }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { dangerouslyBypassSandbox: true, args: ["--sandbox", "read-only"] }, expectedBypass: true }, + { sourceType: "codex_local", sourceRunner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: false, dangerouslyBypassSandbox: true, extraArgs: ["--color", "never"] }, expectedBypass: false }, + ])("preserves the $sourceType runner and saved policy with bypass=$expectedBypass", async ({ sourceType, sourceRunner, sourcePolicy, expectedBypass }) => { mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" }); mockAdaptersApi.list.mockResolvedValue([ { type: "claude_local", disabled: false }, @@ -1284,22 +1284,15 @@ describe("CompanyImport", () => { await enterGithubUrl(); await clickButton(text => text === "Preview import"); await clickButton(text => text.includes("configure adapter")); - await clickButton(text => text.includes("Advanced")); - const select = container.querySelector('[aria-label="Runner"]'); - expect(select).toBeTruthy(); - await act(async () => select!.click()); - const option = document.querySelector(`[role="option"][data-value="${runner}"]`); - expect(option).toBeTruthy(); - expect(option!.disabled).toBe(false); - await act(async () => option!.click()); - await flushReact(); - if (expectedBypass !== undefined) { + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); + if (sourceType === "codex_local") await clickButton(text => text === "Advanced"); + if (sourceType === "codex_local") { expect(container.querySelector('[role="switch"][aria-label="Bypass sandbox"]')?.getAttribute("aria-checked")) .toBe(String(expectedBypass)); } await clickButton(text => text.startsWith("Import 3 file")); await settle(); - expect(lastImportMeta().adapterOverrides).toEqual({ coder: { adapterType: sourceType === "paperclip_runner" ? "paperclip_runner" : "codex_local", runner } }); + expect(lastImportMeta().adapterOverrides?.coder).toBeUndefined(); expect(preview.manifest.agents[0]!.adapterConfig).toEqual(savedSourceConfig); }); @@ -1312,19 +1305,14 @@ describe("CompanyImport", () => { const preview = buildPreviewResult(); const source = { provider: "acpx", acpxAgent: "claude", model: "claude-opus-5-5", modelReasoningEffort: "high", acpxPermissionMode: permission, timeoutSec: 180, lifecycleMode: "per_turn" }; preview.manifest.agents[0]!.adapterType = sourceType; - preview.manifest.agents[0]!.runner = sourceType === "paperclip_runner" ? "paperclip" : "legacy"; + preview.manifest.agents[0]!.runner = "paperclip"; preview.manifest.agents[0]!.adapterConfig = source; mockCompaniesApi.importPreview.mockResolvedValue(preview); await renderPage(); await enterGithubUrl(); await clickButton(text => text === "Preview import"); await clickButton(text => text.includes("configure adapter")); - if (sourceType === "claude_local") { - await clickButton(text => text.includes("Advanced")); - await act(async () => container.querySelector('[aria-label="Runner"]')!.click()); - await act(async () => document.querySelector('[role="option"][data-value="paperclip"]')!.click()); - await flushReact(); - } + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); const select = container.querySelector('[aria-label="Permission mode"]'); if (sourceType === "paperclip_runner") expect(select?.textContent).toContain(permission === "deny-all" ? "Deny all" : "Allow Paperclip reads"); await act(async () => select!.click()); @@ -1342,7 +1330,7 @@ describe("CompanyImport", () => { expect(preview.manifest.agents[0]!.adapterConfig).toEqual(source); }); - it("keeps native Claude configuration at the server translation boundary when switching an import to Legacy", async () => { + it("preserves native Claude import configuration without a runner override", async () => { mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" }); mockAdaptersApi.list.mockResolvedValue([ { type: "claude_local", disabled: false, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" }, @@ -1358,13 +1346,11 @@ describe("CompanyImport", () => { await enterGithubUrl(); await clickButton(text => text === "Preview import"); await clickButton(text => text.includes("configure adapter")); - await clickButton(text => text.includes("Advanced")); - await act(async () => container.querySelector('[aria-label="Runner"]')!.click()); - await act(async () => document.querySelector('[role="option"][data-value="legacy"]')!.click()); - await flushReact(); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect([...container.querySelectorAll("button")].some(button => button.textContent === "Advanced")).toBe(false); await clickButton(text => text.startsWith("Import 3 file")); await settle(); - expect(lastImportMeta().adapterOverrides?.coder).toMatchObject({ adapterType: "paperclip_runner", runner: "legacy" }); + expect(lastImportMeta().adapterOverrides?.coder).toBeUndefined(); expect(preview.manifest.agents[0]!.adapterConfig).toEqual(source); }); @@ -1400,7 +1386,7 @@ describe("CompanyImport", () => { expect(lastImportMeta().adapterOverrides?.coder?.adapterConfig).not.toHaveProperty("codexLegacySandboxBypass"); }); - it.each(["codex_local", "claude_local"].flatMap(harness => ["model-clear", "effort-clear", "env-remove", "legacy-model", "changed-harness"].map(edit => ({ harness, edit }))))("preserves native source configuration during an ordinary import edit ($harness, $edit)", async ({ harness, edit }) => { + it.each(["codex_local", "claude_local"].flatMap(harness => ["model-clear", "effort-clear", "env-remove", "changed-harness"].map(edit => ({ harness, edit }))))("preserves native source configuration during an ordinary import edit ($harness, $edit)", async ({ harness, edit }) => { mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" }); mockAdaptersApi.list.mockResolvedValue([ { type: "claude_local", disabled: false, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" }, @@ -1432,13 +1418,9 @@ describe("CompanyImport", () => { }); } await clickButton(text => text.includes("configure adapter")); - await clickButton(text => text.includes("Advanced")); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); const expected: Record = structuredClone(sourceConfig); - if (edit === "legacy-model") { - await act(async () => container.querySelector('[aria-label="Runner"]')!.click()); - await act(async () => document.querySelector('[role="option"][data-value="legacy"]')!.click()); - } - if (edit === "model-clear" || edit === "legacy-model" || edit === "changed-harness") { + if (edit === "model-clear" || edit === "changed-harness") { await act(async () => container.querySelector('[aria-label="Model"]')!.click()); await act(async () => [...document.querySelectorAll('[role="option"]')].find(option => option.textContent?.trim().startsWith("Default"))!.click()); delete expected.model; @@ -1465,7 +1447,7 @@ describe("CompanyImport", () => { return; } expect(lastImportMeta().adapterOverrides?.coder).toEqual({ - adapterType: "paperclip_runner", runner: edit === "legacy-model" ? "legacy" : "paperclip", adapterConfig: expected, + adapterType: "paperclip_runner", runner: "paperclip", adapterConfig: expected, }); expect(preview.manifest.agents[0]!.adapterConfig).toEqual(sourceConfig); }); diff --git a/ui/src/pages/CompanyImport.tsx b/ui/src/pages/CompanyImport.tsx index 56ee0108345..67f264ce641 100644 --- a/ui/src/pages/CompanyImport.tsx +++ b/ui/src/pages/CompanyImport.tsx @@ -1693,7 +1693,7 @@ export function CompanyImport() { const available = nativeHarness ? runnerInstalled : adapterChoice === "openai_dot" ? dotAvailable : adapterChoice === "muse" ? museAvailable : nativeRunnerAvailable; if (!available && (availableAdapterTypes || !nativeHarness)) { availabilityWarning = nativeHarness - ? "Paperclip Runner is unavailable here. Choose a runner or another harness explicitly." + ? "Paperclip Runner is unavailable here. Choose another harness." : `Paperclip Runner provider ${String(a.adapterConfig?.provider)} requires its experimental feature to be enabled. This import keeps its saved harness until you choose another one.`; } } else if (availableAdapterTypes && !availableAdapterTypes.has(a.adapterType)) { diff --git a/ui/src/pages/NewAgent.test.tsx b/ui/src/pages/NewAgent.test.tsx index 7efcba41c27..8d934f11fb8 100644 --- a/ui/src/pages/NewAgent.test.tsx +++ b/ui/src/pages/NewAgent.test.tsx @@ -161,10 +161,14 @@ describe("New agent setup", () => { expect(state.navigate).toHaveBeenCalledWith(expect.stringContaining(`setupAgentId=${setupId}`), { replace: true }); expect(api.hire).not.toHaveBeenCalled(); expect(api.testEnvironment).not.toHaveBeenCalled(); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect([...container.querySelectorAll("summary")].some(summary => summary.textContent === "Advanced")).toBe(false); await click("OpenAIAPI key"); await fill("API key", "setup-test-key"); await click("Connect"); expect(api.testEnvironment.mock.calls[0][2].agentId).toBe(setupId); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect([...container.querySelectorAll("summary")].some(summary => summary.textContent === "Advanced")).toBe(false); expect(api.update).toHaveBeenCalledWith(setupId, expect.objectContaining({ runtimeConfig: expect.objectContaining({ aiConnection: expect.anything() }) }), "company-1"); await click("Finish setup"); expect(api.hire.mock.calls[0][1].setupAgentId).toBe(setupId); @@ -475,7 +479,7 @@ describe("New agent setup", () => { expect(api.hire.mock.calls[0][1]).not.toHaveProperty("runner"); }); - it("records an explicit legacy choice when it matches the Windows default, then preserves it on Linux", async () => { + it("restores a legacy setup link and preserves it when changing environments", async () => { settings.get.mockResolvedValue({ defaultEnvironmentId: "local-1" }); envApi.list.mockResolvedValue([ { id: "local-1", name: "Windows", driver: "local", status: "active", config: {} }, @@ -483,12 +487,9 @@ describe("New agent setup", () => { ]); adapterApi.list.mockImplementation(async (context?: { environmentId?: string | null }) => state.adapters.map((adapter: any) => adapter.type === "codex_local" ? { ...adapter, supportedRunners: context?.environmentId === "linux-ssh" ? ["paperclip", "legacy"] : ["legacy"], defaultRunner: context?.environmentId === "linux-ssh" ? "paperclip" : "legacy" } : adapter)); - await render("codex_local"); - await act(async () => (container.querySelector("summary") as HTMLElement).click()); - const runner = container.querySelector('[aria-label="Runner"]')!; - expect(runner.textContent).toBe("Legacy runner (default)"); - await act(async () => runner.click()); - await act(async () => document.querySelector('[role="option"][data-value="legacy"]')!.click()); + state.params = new URLSearchParams({ name: "Atlas", adapterType: "codex_local", runner: "legacy" }); + await renderCurrentParams(); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); await connect("OpenAI"); expect(api.testEnvironment).toHaveBeenLastCalledWith("company-1", "codex_local", expect.objectContaining({ runner: "legacy" })); const environment = container.querySelector('select[aria-label="Environment"]')!; @@ -523,8 +524,8 @@ describe("New agent setup", () => { adapterApi.list.mockImplementation(async (context?: { companyId: string; environmentId?: string | null }) => state.adapters.map((adapter: any) => adapter.type === "codex_local" ? { ...adapter, supportedRunners: ["paperclip", "legacy"], defaultRunner: context?.environmentId === "linux-ssh" ? "paperclip" : "legacy" } : adapter)); await render("codex_local"); - await act(async () => (container.querySelector("summary") as HTMLElement).click()); - expect(container.querySelector('[aria-label="Runner"]')?.textContent).toBe("Paperclip Runner (default)"); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect(container.querySelector("summary")).toBeNull(); expect(adapterApi.list).toHaveBeenCalledWith({ companyId: "company-1", environmentId: "linux-ssh" }); await connect("OpenAI"); expect(api.testEnvironment.mock.calls[0][2].environmentId).toBe("linux-ssh"); @@ -534,40 +535,42 @@ describe("New agent setup", () => { environment.dispatchEvent(new Event("change", { bubbles: true })); }); await settle(); - await act(async () => (container.querySelector("summary") as HTMLElement).click()); - expect(container.querySelector('[aria-label="Runner"]')?.textContent).toBe("Legacy runner (default)"); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect(container.querySelector("summary")).toBeNull(); expect(adapterApi.list).toHaveBeenCalledWith({ companyId: "company-1", environmentId: "local-1" }); await click("Finish setup"); expect(api.testEnvironment).toHaveBeenCalledTimes(2); expect(api.testEnvironment).toHaveBeenLastCalledWith("company-1", "codex_local", expect.objectContaining({ environmentId: "local-1" })); expect(api.hire.mock.calls[0][1]).not.toHaveProperty("runner"); }); - it("surfaces failed target discovery without guessing a default and permits explicit legacy recovery", async () => { + it("surfaces failed target discovery and retries automatic selection without a runner dropdown", async () => { adapterApi.list.mockImplementation(async (context?: object) => { if (context) throw new Error("SSH target is unavailable. Check its connection."); return state.adapters; }); await render("codex_local"); await settle(); - await act(async () => (container.querySelector("summary") as HTMLElement).click()); - expect(container.querySelector('[aria-label="Runner"]')?.textContent).toBe("Automatic selection unavailable"); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect(container.querySelector("summary")).toBeNull(); expect(container.textContent).toContain("SSH target is unavailable. Check its connection."); expect(container.textContent).not.toContain("Legacy runner (default)"); - expect(container.textContent).toContain("Retry runner discovery"); - await act(async () => container.querySelector('[aria-label="Runner"]')!.click()); - await act(async () => [...document.querySelectorAll('[role="option"]')].find(option => option.textContent === "Legacy runner")!.click()); + expect(api.testEnvironment).not.toHaveBeenCalled(); + expect(api.hire).not.toHaveBeenCalled(); + adapterApi.list.mockResolvedValue(state.adapters); + await click("Retry"); + expect(container.textContent).not.toContain("SSH target is unavailable. Check its connection."); await connect("OpenAI"); await click("Finish setup"); - expect(api.testEnvironment.mock.calls[0][2]).toMatchObject({ runner: "legacy" }); - expect(api.hire.mock.calls[0][1]).toMatchObject({ runner: "legacy" }); + expect(api.testEnvironment.mock.calls[0][2]).not.toHaveProperty("runner"); + expect(api.hire.mock.calls[0][1]).not.toHaveProperty("runner"); }); it.each(["Linux x64", "Mac arm64", "Mac x64"])("uses automatic Codex resolution through setup and hire without the experimental gate (%s)", async name => { settings.getExperimental.mockResolvedValue({ enableNativeRunner: false }); envApi.list.mockResolvedValue([{ id: "local-1", name, driver: "local", status: "active", config: {} }]); state.adapters = (state.adapters as any[]).map(adapter => adapter.type === "codex_local" ? { ...adapter, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" } : adapter); await render("codex_local"); - await act(async () => (container.querySelector("summary") as HTMLElement).click()); - expect(container.querySelector('[aria-label="Runner"]')?.textContent).toBe("Paperclip Runner (default)"); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect(container.querySelector("summary")).toBeNull(); await connect("OpenAI"); await click("Finish setup"); expect(api.hire.mock.calls[0][1].adapterType).toBe("codex_local"); @@ -577,21 +580,6 @@ describe("New agent setup", () => { expect(api.hire.mock.calls[0][1].adapterConfig.dangerouslyBypassApprovalsAndSandbox).not.toBe(true); }); - it("allows the explicit legacy override before authentication and rechecks it after a runner change", async () => { - state.adapters = (state.adapters as any[]).map(adapter => adapter.type === "codex_local" ? { ...adapter, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" } : adapter); - await render("codex_local"); - await act(async () => (container.querySelector("summary") as HTMLElement).click()); - await act(async () => container.querySelector('[aria-label="Runner"]')!.click()); - await act(async () => [...document.querySelectorAll('[role="option"]')].find(option => option.textContent === "Legacy runner")!.click()); - await connect("OpenAI"); - expect(api.testEnvironment.mock.calls[0][2].runner).toBe("legacy"); - await act(async () => (container.querySelector("summary") as HTMLElement).click()); - await act(async () => container.querySelector('[aria-label="Runner"]')!.click()); - await act(async () => [...document.querySelectorAll('[role="option"]')].find(option => option.textContent === "Paperclip Runner (default)")!.click()); - await click("Finish setup"); - expect(api.testEnvironment).toHaveBeenLastCalledWith("company-1", "codex_local", expect.objectContaining({ runner: "paperclip" })); - expect(api.hire.mock.calls[0][1]).toMatchObject({ adapterType: "codex_local", runner: "paperclip" }); - }); it("opens pending Antigravity configuration without creating or admitting a native agent", async () => { await render("paperclip_runner", "antigravity"); expect(api.create).not.toHaveBeenCalled(); @@ -898,7 +886,8 @@ describe("New agent setup", () => { await render("paperclip_runner", "claude"); expect(container.textContent).not.toContain("This adapter is unavailable"); expect(container.textContent).toContain("Claude Code"); - expect(container.textContent).toContain("Paperclip Runner"); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); + expect(container.querySelector("summary")).toBeNull(); expect(container.textContent).toContain("Connect a model"); expect(api.hire).not.toHaveBeenCalled(); }); @@ -1006,13 +995,12 @@ describe("New agent setup", () => { })); expect(JSON.stringify(api.hire.mock.calls)).not.toContain("native-cursor-test-key"); }); - it("preserves the ordinary Cursor Cloud Advanced Runner choice through test and finish", async () => { + it("preserves a saved Cursor Cloud runner choice through test and finish", async () => { adapterApi.list.mockImplementation(async () => state.adapters.map((adapter: any) => adapter.type === "cursor_cloud" ? { ...adapter, supportedRunners: ["legacy", "paperclip"], defaultRunner: "legacy" } : adapter)); - await render("cursor_cloud"); - await act(async () => [...container.querySelectorAll("summary")].find(item => item.textContent === "Advanced")!.click()); - await act(async () => container.querySelector('[aria-label="Runner"]')!.click()); - await act(async () => document.querySelector('[role="option"][data-value="paperclip"]')!.click()); + state.params = new URLSearchParams({ name: "Atlas", adapterType: "cursor_cloud", runner: "paperclip" }); + await renderCurrentParams(); + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); await settle(); await fill("GitHub repository", "https://github.com/acme/repo"); await fill("Branch", "feature"); @@ -1479,15 +1467,11 @@ describe("New agent setup", () => { settings.getExperimental.mockResolvedValue({ enableNativeRunner: false }); state.adapters = state.adapters.map((adapter: any) => adapter.type === "opencode_local" ? { ...adapter, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" } : adapter); - await render("opencode_local"); + state.params = new URLSearchParams({ name: "Atlas", adapterType: "opencode_local", ...(runner === "legacy" ? { runner } : {}) }); + await renderCurrentParams(); const model = "openrouter/anthropic/claude-sonnet-4.6"; await fill("Model", model); - const select = container.querySelector('[aria-label="Runner"]')!; - expect(select.textContent).toContain("Paperclip Runner"); - if (runner === "legacy") { - await act(async () => select.click()); - await act(async () => document.querySelector('[role="option"][data-value="legacy"]')!.click()); - } + expect(container.querySelector('[aria-label="Runner"]')).toBeNull(); await click("Run test"); await click("Finish setup"); expect(api.testEnvironment).toHaveBeenCalledWith("company-1", "opencode_local", expect.objectContaining({ adapterConfig: expect.objectContaining({ model }), ...(runner === "legacy" ? { runner } : {}) })); diff --git a/ui/storybook/stories/agents/agent-settings-redesign.stories.tsx b/ui/storybook/stories/agents/agent-settings-redesign.stories.tsx index 9564c92a678..827ac352830 100644 --- a/ui/storybook/stories/agents/agent-settings-redesign.stories.tsx +++ b/ui/storybook/stories/agents/agent-settings-redesign.stories.tsx @@ -93,7 +93,9 @@ export const CodexNativeMobile: Story = { ...RunnerRuntime, globals: { viewport: export const CodexLegacyAdvanced: Story = { ...CodexRuntime, play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(await canvas.findByRole("button", { name: "Advanced" })); - await expect(await canvas.findByRole("button", { name: "Runner" })).toHaveTextContent("Legacy runner"); + await expect(canvas.queryByRole("button", { name: "Runner" })).not.toBeInTheDocument(); + await expect(await canvas.findByText("Command", { exact: true })).toBeVisible(); + await expect(await canvas.findByText("Extra args (comma-separated)", { exact: true })).toBeVisible(); } }; export const Secrets: Story = { name: "Secrets & variables", diff --git a/ui/storybook/stories/agents/codex-runner/creation.stories.tsx b/ui/storybook/stories/agents/codex-runner/creation.stories.tsx index c6da4db9585..bf26e5465ec 100644 --- a/ui/storybook/stories/agents/codex-runner/creation.stories.tsx +++ b/ui/storybook/stories/agents/codex-runner/creation.stories.tsx @@ -34,7 +34,7 @@ function installCodexCreationFixture({ setupFailure = false, nativeRunnerEnabled const native = request.runner === "paperclip" || request.adapterType === "paperclip_runner" || (request.runner !== "legacy" && !unqualifiedTarget); return Response.json({ adapterType: native ? "paperclip_runner" : "codex_local", testedAt: new Date(0).toISOString(), status: setupFailure && native ? "fail" : "pass", - checks: setupFailure && native ? [{ code: "runner_missing", level: "error", message: "Paperclip Runner is missing from this environment.", hint: "Install the runner or choose Legacy runner in Advanced." }] + checks: setupFailure && native ? [{ code: "runner_missing", level: "error", message: "Paperclip Runner is missing from this environment.", hint: "Install the runner in the selected environment, then retry setup." }] : [{ code: native ? "paperclip_runner_codex_hello_probe_passed" : "codex_hello_probe_passed", level: "info", message: "The selected runtime responded." }], }); } @@ -65,7 +65,7 @@ const meta = { status: "current", implementation: "production", boundary: "fixtures", - notes: "Production NewAgent and AgentBasicsDialog with simulated accounts, target qualification, setup and hire replies. Covers automatic and Legacy choices, old native links and setup failures; no live credentials, cleanup, persistence or provider execution.", + notes: "Production NewAgent and AgentBasicsDialog with simulated accounts, target qualification, setup and hire replies. Covers automatic selection, saved Legacy choices, old native links and setup failures; no live credentials, cleanup, persistence or provider execution.", }, }, beforeEach: ({ parameters }) => installCodexCreationFixture(parameters.codexCreationFixture), @@ -73,10 +73,11 @@ const meta = { } satisfies Meta; export default meta; type Story = StoryObj; -const showRunner: NonNullable = async ({ canvasElement }) => { +const expectRunnerHidden: NonNullable = async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click(await canvas.findByText("Advanced", { exact: true, selector: "summary" })); - await expect(await canvas.findByRole("button", { name: "Runner" })).toBeVisible(); + await expect(await canvas.findByRole("heading", { name: "Connect a model" })).toBeVisible(); + await expect(canvas.queryByRole("button", { name: "Runner" })).not.toBeInTheDocument(); + await expect(canvas.queryByText("Advanced", { exact: true, selector: "summary" })).not.toBeInTheDocument(); }; const connectCodex: NonNullable = async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -86,25 +87,21 @@ const connectCodex: NonNullable = async ({ canvasElement }) => { await userEvent.click(connect); }; export const AutomaticCodex: Story = { play: async context => { - await showRunner(context); - await expect(await within(context.canvasElement).findByRole("button", { name: "Runner" })).toHaveTextContent("Paperclip Runner (default)"); + await expectRunnerHidden(context); } }; export const ExplicitLegacy: Story = { parameters: { initialEntries: ["/PAP/agents/new?name=Nova&adapterType=codex_local&runner=legacy"] }, play: async context => { - await showRunner(context); - await expect(await within(context.canvasElement).findByRole("button", { name: "Runner" })).toHaveTextContent("Legacy runner"); + await expectRunnerHidden(context); } }; export const AutomaticLegacyOnUnqualifiedTarget: Story = { parameters: { codexCreationFixture: { unqualifiedTarget: true } }, play: async context => { - await showRunner(context); - await expect(await within(context.canvasElement).findByRole("button", { name: "Runner" })).toHaveTextContent("Legacy runner (default)"); + await expectRunnerHidden(context); }, }; export const OldNativeCodexLink: Story = { parameters: { initialEntries: ["/PAP/agents/new?name=Nova&adapterType=paperclip_runner&runnerProvider=codex"], codexCreationFixture: { unqualifiedTarget: true } }, play: async context => { - await showRunner(context); - await expect(await within(context.canvasElement).findByRole("button", { name: "Runner" })).toHaveTextContent("Paperclip Runner"); + await expectRunnerHidden(context); }, }; export const AutomaticLight: Story = { ...AutomaticCodex, globals: { theme: "light" } }; @@ -112,7 +109,7 @@ export const LegacyMobile: Story = { ...ExplicitLegacy, globals: { viewport: { v export const SetupFailure: Story = { parameters: { codexCreationFixture: { setupFailure: true } }, play: async context => { await connectCodex(context); await expect(await within(context.canvasElement).findByText("Paperclip Runner is missing from this environment.")).toBeVisible(); - await showRunner(context); + await expectRunnerHidden(context); } }; export const TaskReady: Story = { play: async context => { diff --git a/ui/storybook/stories/onboarding/onboarding-agent-arc.stories.tsx b/ui/storybook/stories/onboarding/onboarding-agent-arc.stories.tsx index a70e7dccf89..77ae7c978fa 100644 --- a/ui/storybook/stories/onboarding/onboarding-agent-arc.stories.tsx +++ b/ui/storybook/stories/onboarding/onboarding-agent-arc.stories.tsx @@ -623,8 +623,8 @@ function installCodexPlatformFixture(outcome: CodexPlatformFixtureOutcome) { codexPlatformFixtureReceipt = receipt; let qualified = false; const diagnostic = outcome === "permission" - ? "Instance admin access is required to qualify this environment. Ask an instance admin or choose Legacy runner in Advanced." - : "The setup lease could not be confirmed as released. Ask an instance admin to resolve cleanup or choose Legacy runner in Advanced."; + ? "Instance admin access is required to qualify this environment. Ask an instance admin to qualify this environment." + : "The setup lease could not be confirmed as released. Ask an instance admin to resolve cleanup, then retry setup."; const fixtureFetch: typeof fetch = async (input, init) => { const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url, window.location.origin); const method = init?.method ?? (input instanceof Request ? input.method : "GET"); @@ -697,15 +697,10 @@ function managedCodexQualificationStory(outcome: CodexPlatformFixtureOutcome): S await screen.findByRole("heading", { name: "Let's get started..." }, { timeout: STEP_TIMEOUT_MS }); } else { await screen.findByText(outcome === "permission" - ? "Instance admin access is required to qualify this environment. Ask an instance admin or choose Legacy runner in Advanced." - : "The setup lease could not be confirmed as released. Ask an instance admin to resolve cleanup or choose Legacy runner in Advanced.", {}, { timeout: STEP_TIMEOUT_MS }); - await userEvent.click(screen.getByText("Advanced", { exact: true, selector: "summary" })); - const runnerControl = screen.getByRole("button", { name: "Runner" }); - await expect(runnerControl).toHaveTextContent("Legacy runner (default)"); - await waitFor(() => expect(runnerControl).toBeEnabled(), { timeout: STEP_TIMEOUT_MS }); - await userEvent.click(runnerControl); - await expect(await screen.findByRole("option", { name: "Legacy runner (default)" })).toBeEnabled(); - await userEvent.keyboard("{Escape}"); + ? "Instance admin access is required to qualify this environment. Ask an instance admin to qualify this environment." + : "The setup lease could not be confirmed as released. Ask an instance admin to resolve cleanup, then retry setup.", {}, { timeout: STEP_TIMEOUT_MS }); + await expect(screen.queryByRole("button", { name: "Runner" })).not.toBeInTheDocument(); + await expect(screen.queryByText("Advanced", { exact: true, selector: "summary" })).not.toBeInTheDocument(); await expect(screen.queryByRole("heading", { name: "Let's get started..." })).not.toBeInTheDocument(); } const receipt = codexPlatformFixtureReceipt!;