feat: default Claude to the native runner on Apple Silicon

Carry Claude effort through versioned native admission and recovery. Preserve existing connections and saved runner choices, and price Opus 5.5 API receipts.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 10:48:21 -05:00
1 parent 058af8f1b6
commit 6fd56c445d
77 files changed
+909 -296

No files matched your search

+2 -1
View File
@@ -212,7 +212,8 @@ export function getAdapterDisplay(type: string): AdapterDisplayInfo {
export function agentDisplayAdapterType(agent: { adapterType: string; adapterConfig: Record<string, unknown> }): string {
if (agent.adapterType !== "paperclip_runner") return agent.adapterType;
if (agent.adapterConfig.provider === "openai_dot") return "openai_dot";
return agentHarnessType(agent.adapterType, agent.adapterConfig) === "codex_local" ? "codex_local" : agent.adapterType;
const harness = agentHarnessType(agent.adapterType, agent.adapterConfig);
return ["codex_local", "claude_local"].includes(harness) ? harness : agent.adapterType;
}
export function isKnownAdapterType(type: string): boolean {
@@ -35,7 +35,7 @@ const inputClass =
const instructionsFileHint =
"Absolute path to a markdown file (e.g. AGENTS.md) that defines this agent's behavior. Injected into the system prompt at runtime. Note: Codex may still auto-apply repo-scoped AGENTS.md files from the workspace.";
const defaultOpenCodeRunnerModel = "openrouter/deepseek/deepseek-v4-flash-0731";
const defaultAcpxClaudeModel = "claude-sonnet-5";
const defaultAcpxClaudeModel = "claude-opus-5-5";
const defaultClaudeManagedModel = "claude-sonnet-5";
const defaultAwsAgentCoreModel = "global.anthropic.claude-sonnet-4-6";
const runnerHarnessOptions = [
@@ -849,7 +849,7 @@ describe("AgentConfigForm environment selector", () => {
it.each([
["Codex", "codex", undefined, DEFAULT_CODEX_LOCAL_MODEL],
["ACP agents", "acpx", "claude", "claude-sonnet-5"],
["ACP agents", "acpx", "claude", "claude-opus-5-5"],
["Claude Managed", "claude_managed", undefined, "claude-sonnet-5"],
])("saves the %s harness default without the previous OpenCode model prefix", async (label, provider, acpxAgent, model) => {
const accountList = vi.spyOn(aiConnectionsApi, "list").mockResolvedValue({
@@ -996,8 +996,8 @@ describe("AgentConfigForm environment selector", () => {
roots.push(existing.root);
const created = await renderCreateForm(environments, { adapterType: "claude_local", model: "" });
roots.push(created.root);
expect(existing.container.textContent).toContain("Default (claude-opus-5)");
expect(created.container.textContent).toContain("Default (claude-opus-5)");
expect(existing.container.textContent).toContain("Default (claude-opus-5-5)");
expect(created.container.textContent).toContain("Default (claude-opus-5-5)");
expect(existing.onSave).not.toHaveBeenCalled();
});
+32 -29
View File
@@ -1,7 +1,7 @@
import { useConnectionModels } from "./ai-connections/useConnectionModels";
import { agentHarnessType, agentRunner, type AgentRunnerChoice } from "@paperclipai/shared";
import { adaptersApi } from "../api/adapters";
import { CodexRunnerSelect } from "./CodexRunnerSelect";
import { AgentRunnerSelect } from "./AgentRunnerSelect";
import { aiRoutingHarness } from "@paperclipai/shared";
import { AiConnectionField } from "./ai-connections/AiConnectionField";
import { aiConnectionBindingSchema, aiRuntimeConnectionBindingSchema } from "@paperclipai/shared";
@@ -609,6 +609,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const effectiveConfig = isCreate ? props.values.adapterSchemaValues ?? {} : { ...config, ...overlay.adapterConfig };
const harnessType = agentHarnessType(adapterType, effectiveConfig);
const isCodexHarness = harnessType === "codex_local";
const isRunnerHarness = isCodexHarness || harnessType === "claude_local";
const rawCurrentDefaultEnvironmentId = isCreate
? props.values.defaultEnvironmentId ?? ""
: eff("identity", "defaultEnvironmentId", props.agent.defaultEnvironmentId ?? "");
@@ -622,13 +623,13 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
queryFn: () => adaptersApi.list({ companyId: selectedCompanyId!, environmentId: rawCurrentDefaultEnvironmentId || null }),
enabled: Boolean(selectedCompanyId),
});
const codexRunnerAvailability = runnerDiscovery.data?.find(item => item.type === "codex_local");
const runnerAvailability = runnerDiscovery.data?.find(item => item.type === harnessType);
const runner: AgentRunnerChoice = isCreate ? props.values.runner ?? "auto" : overlay.runner ?? agentRunner(adapterType);
const fieldsAdapterType = isCodexHarness
? (runner === "paperclip" || (runner === "auto" && codexRunnerAvailability?.defaultRunner === "paperclip") ? "paperclip_runner" : "codex_local")
const fieldsAdapterType = isRunnerHarness
? (runner === "paperclip" || (runner === "auto" && runnerAvailability?.defaultRunner === "paperclip") ? "paperclip_runner" : harnessType)
: adapterType;
const getCapabilities = useAdapterCapabilities();
const adapterCaps = getCapabilities(isCodexHarness ? harnessType : adapterType);
const adapterCaps = getCapabilities(isRunnerHarness ? harnessType : adapterType);
const isDotRunner = adapterType === "paperclip_runner" && (isCreate ? props.values.adapterSchemaValues?.provider : eff("adapterConfig", "provider", config.provider)) === "openai_dot";
const isLocal = !isDotRunner && (adapterCaps.supportsInstructionsBundle || adapterCaps.supportsSkills || adapterCaps.supportsLocalAgentJwt);
@@ -918,7 +919,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
).data?.provider === "openrouter" ? "openrouter" : runnerProvider;
const connectionModels = useConnectionModels(selectedCompanyId, isCreate ? undefined : aiConnectionBindingSchema.safeParse((overlay.runtime.runtimeConfig as Record<string, unknown> | undefined)?.aiConnection ?? runtimeConfig.aiConnection).data, aiRoutingHarness(adapterType, runnerProvider, eff("adapterConfig", "acpxAgent", config.acpxAgent)));
// Fetch adapter models for the effective provider, including unsaved changes.
const modelAdapterType = isCodexHarness ? harnessType : adapterType;
const modelAdapterType = isRunnerHarness ? harnessType : adapterType;
const modelQueryKey = selectedCompanyId
? queryKeys.agents.adapterModels(selectedCompanyId, modelAdapterType, currentDefaultEnvironmentId || null, modelProvider)
: ["agents", "none", "adapter-models", adapterType];
@@ -970,10 +971,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
mode,
isCreate,
adapterType: fieldsAdapterType,
hideRunnerHarness: isCodexHarness,
values: isCreate ? props.values : null,
hideRunnerHarness: isRunnerHarness,
values: isCreate ? { ...props.values, ...(harnessType === "claude_local" && fieldsAdapterType === "paperclip_runner" ? { adapterSchemaValues: { ...props.values.adapterSchemaValues, provider: "acpx", acpxAgent: "claude" } } : {}) } : null,
set: isCreate ? (patch: Partial<CreateConfigValues>) => props.onChange(patch) : null,
config,
config: harnessType === "claude_local" && fieldsAdapterType === "paperclip_runner" ? { ...config, provider: "acpx", acpxAgent: "claude" } : config,
eff: eff as <T>(group: "adapterConfig", field: string, original: T) => T,
// Harness transitions supply the new harness's default model. Resolve
// user-selected model IDs in ModelDropdown, not through the previous
@@ -1104,9 +1105,9 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
: adapterConfig.provider === "acpx" && adapterConfig.acpxAgent === "claude" ? "claude_local"
: adapterType
: adapterType;
return testAgentSetup({ companyId: selectedCompanyId, adapterType, providerAdapter, adapterConfig, agentId, aiConnection, environmentId, ...(isCodexHarness && (isCreate ? runner !== "auto" : overlay.runner !== undefined) ? { runner } : {}) });
return testAgentSetup({ companyId: selectedCompanyId, adapterType, providerAdapter, adapterConfig, agentId, aiConnection, environmentId, ...(isRunnerHarness && (isCreate ? runner !== "auto" : overlay.runner !== undefined) ? { runner } : {}) });
}
return agentsApi.testEnvironment(selectedCompanyId, adapterType, { adapterConfig, agentId, aiConnection, environmentId, ...(isCodexHarness && (isCreate ? runner !== "auto" : overlay.runner !== undefined) ? { runner } : {}) });
return agentsApi.testEnvironment(selectedCompanyId, adapterType, { adapterConfig, agentId, aiConnection, environmentId, ...(isRunnerHarness && (isCreate ? runner !== "auto" : overlay.runner !== undefined) ? { runner } : {}) });
},
});
const [testActionPending, setTestActionPending] = useState(false);
@@ -1153,7 +1154,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
resetTestEnvironmentRef.current();
setTestActionError(null);
clearClaudeLoginClaimRef.current();
}, [adapterType, runner, effectiveLoginEnvironmentId, runner === "auto" ? codexRunnerAvailability?.defaultRunner : undefined]);
}, [adapterType, runner, effectiveLoginEnvironmentId, runner === "auto" ? runnerAvailability?.defaultRunner : undefined]);
// Show the login affordance only for a current sandbox adapter that declares a
// login capability, and whose most recent Test result carries the canonical
@@ -1164,7 +1165,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const adapterSupportsSandboxLogin = adapterCaps.login != null;
const testResult = testEnvironment.data;
const testResultSupportsSandboxLogin =
testResult != null && getCapabilities(isCodexHarness ? harnessType : testResult.adapterType).login != null;
testResult != null && getCapabilities(isRunnerHarness ? harnessType : testResult.adapterType).login != null;
const authMissingCheck =
testResult && testResultSupportsSandboxLogin
? testResult.checks.find((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE) ?? null
@@ -1259,7 +1260,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
// environment id, so both are present here.
login:
showAdapterLogin && selectedCompanyId && effectiveLoginEnvironmentId
? { companyId: selectedCompanyId, adapterType: isCodexHarness ? harnessType : adapterType, environmentId: effectiveLoginEnvironmentId }
? { companyId: selectedCompanyId, adapterType: isRunnerHarness ? harnessType : adapterType, environmentId: effectiveLoginEnvironmentId }
: null,
});
return () => {
@@ -1297,7 +1298,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
}
const thinkingEffortKey =
isCodexHarness
isCodexHarness || (adapterType === "paperclip_runner" && harnessType === "claude_local")
? "modelReasoningEffort"
: adapterType === "cursor"
? "mode"
@@ -1319,19 +1320,19 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
? kimiThinkingEffortOptions
: adapterType === "pi_local"
? [{ id: "", label: "Auto" }, ...["off", "minimal", "low", "medium", "high", "xhigh"].map(id => ({ id, label: id }))]
: adapterType === "claude_local" || adapterType === "grok_local"
? [{ id: "", label: "Auto" }, ...setupEfforts(adapterType, currentModelId).map((id) => ({
: harnessType === "claude_local" || adapterType === "grok_local"
? [{ id: "", label: "Auto" }, ...setupEfforts(harnessType, currentModelId).map((id) => ({
id,
label: id === "xhigh" ? "X-High" : id[0].toUpperCase() + id.slice(1),
}))]
: claudeThinkingEffortOptions;
const currentThinkingEffort = isCreate
? val!.thinkingEffort
: isCodexHarness
: isCodexHarness || (adapterType === "paperclip_runner" && harnessType === "claude_local")
? eff(
"adapterConfig",
thinkingEffortKey,
String(config[thinkingEffortKey] ?? config.reasoningEffort ?? config.modelReasoningEffort ?? ""),
String(config[thinkingEffortKey] ?? config.reasoningEffort ?? config.modelReasoningEffort ?? config.effort ?? ""),
)
: adapterType === "cursor"
? eff("adapterConfig", "mode", String(config.mode ?? ""))
@@ -1340,7 +1341,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
: eff("adapterConfig", thinkingEffortKey, String(config[thinkingEffortKey] ?? ""));
const showThinkingEffort = adapterType !== "gemini_local"
&& adapterType !== "cursor_cloud"
&& (adapterType !== "paperclip_runner" || isCodexHarness);
&& (adapterType !== "paperclip_runner" || isRunnerHarness);
const codexSearchEnabled = adapterType === "codex_local"
? (isCreate ? Boolean(val!.search) : eff("adapterConfig", "search", Boolean(config.search)))
: false;
@@ -1628,7 +1629,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
{showAdapterTypeField && (
<Field label="Harness" hint={help.adapterType}>
<AdapterTypeDropdown
value={isDotRunner ? "openai_dot" : isCodexHarness ? harnessType : adapterType}
value={isDotRunner ? "openai_dot" : isRunnerHarness ? harnessType : adapterType}
disabledTypes={adapterPickerDisabledTypes}
openAiDotEnabled={experimentalSettings?.enableOpenAiDot === true}
nativeRunnerEnabled={experimentalSettings?.enableNativeRunner === true}
@@ -1664,7 +1665,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
setOverlay((prev) => ({
...prev,
adapterType: t,
runner: t === "codex_local" ? "auto" : undefined,
runner: ["codex_local", "claude_local"].includes(t) ? "auto" : undefined,
adapterConfig: {
model:
t === "gemini_local"
@@ -1720,7 +1721,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
<AdapterLoginPanel
key={`${adapterType}:${effectiveLoginEnvironmentId}`}
companyId={selectedCompanyId!}
adapterType={isCodexHarness ? harnessType : adapterType}
adapterType={isRunnerHarness ? harnessType : adapterType}
environmentId={effectiveLoginEnvironmentId!}
onStored={isCreate ? handleClaudeLoginStored : handleClaudeLoginStoredEdit}
onApplyStored={
@@ -1762,8 +1763,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
loadingModels={connectionModels?.isLoading ?? fetchingModels}
value={currentModelId}
onChange={(v) => {
const supportedEfforts = setupEfforts(isCodexHarness ? harnessType : adapterType, v);
const clearUnsupportedEffort = ["codex_local", "claude_local", "grok_local"].includes(isCodexHarness ? harnessType : adapterType)
const supportedEfforts = setupEfforts(isRunnerHarness ? harnessType : adapterType, v);
const clearUnsupportedEffort = ["codex_local", "claude_local", "grok_local"].includes(isRunnerHarness ? harnessType : adapterType)
&& Boolean(currentThinkingEffort)
&& !supportedEfforts.includes(String(currentThinkingEffort));
if (isCreate) {
@@ -1831,7 +1832,9 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
? set!({ thinkingEffort: v })
: (() => {
mark("adapterConfig", thinkingEffortKey, v || undefined);
if (isCodexHarness) mark("adapterConfig", thinkingEffortKey === "reasoningEffort" ? "modelReasoningEffort" : "reasoningEffort", undefined);
if (isRunnerHarness) {
for (const key of ["modelReasoningEffort", "reasoningEffort", "effort"]) if (key !== thinkingEffortKey) mark("adapterConfig", key, undefined);
}
})()
}
open={thinkingEffortOpen}
@@ -1893,10 +1896,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
onToggle={() => setConfigurationAdvancedOpen(!configurationAdvancedOpen)}
>
<div className="space-y-3">
{isCodexHarness && <CodexRunnerSelect value={runner}
{isRunnerHarness && <AgentRunnerSelect value={runner}
pending={runnerDiscovery.isFetching} error={runnerDiscovery.error?.message} onRetry={() => { void runnerDiscovery.refetch(); }}
defaultRunner={codexRunnerAvailability?.defaultRunner}
supportedRunners={codexRunnerAvailability?.supportedRunners}
defaultRunner={runnerAvailability?.defaultRunner}
supportedRunners={runnerAvailability?.supportedRunners}
onChange={next => isCreate ? set!({ runner: next }) : setOverlay(prev => ({ ...prev, runner: next }))} />}
{isLocal && (<> {/*
The command names a binary on the execution host, so the
@@ -4,7 +4,7 @@ import { SelectPopover } from "./ui/select";
import { Button } from "./ui/button";
/** The override is request-level. Automatic selection is resolved by the server. */
export function CodexRunnerSelect({
export function AgentRunnerSelect({
value,
onChange,
defaultRunner,
@@ -1,7 +1,7 @@
import { useMemo, useState } from "react";
import { agentHarnessType, agentRunner, type AgentRunnerChoice } from "@paperclipai/shared";
import { adaptersApi } from "@/api/adapters";
import { CodexRunnerSelect } from "./CodexRunnerSelect";
import { AgentRunnerSelect } from "./AgentRunnerSelect";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
@@ -68,12 +68,12 @@ export function ConfigureBuiltInAgentModal({
const { definition } = state;
const [adapterType, setAdapterType] = useState<string>(
() => state.agent && agentHarnessType(state.agent.adapterType, state.agent.adapterConfig) === "codex_local" ? "codex_local" : state.agent?.adapterType ?? defaultAdapterType(state),
() => state.agent && ["codex_local", "claude_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<AgentRunnerChoice | undefined>();
const environmentId = state.agent?.defaultEnvironmentId ?? null;
const adapters = useQuery({ queryKey: queryKeys.adapters.availability(companyId, environmentId), queryFn: () => adaptersApi.list({ companyId, environmentId }), enabled: open });
const codexAvailability = adapters.data?.find(item => item.type === "codex_local");
const runnerAvailability = adapters.data?.find(item => item.type === adapterType);
const [model, setModel] = useState<string>(() => {
const config = state.agent?.adapterConfig;
const configuredModel = typeof config === "object" && config !== null
@@ -145,7 +145,7 @@ export function ConfigureBuiltInAgentModal({
: { adapterType, adapterConfig };
const result = await builtInAgentsApi.provision(companyId, definition.key, {
...adapterInput,
...(adapterType === "codex_local" && runner !== undefined ? { runner } : {}),
...(["codex_local", "claude_local"].includes(adapterType) && runner !== undefined ? { runner } : {}),
...(budgetMonthlyCents !== undefined ? { budgetMonthlyCents } : {}),
});
return result;
@@ -191,11 +191,11 @@ export function ConfigureBuiltInAgentModal({
/>
</Field>
{adapterType === "codex_local" && <details className="space-y-3">
{["codex_local", "claude_local"].includes(adapterType) && <details className="space-y-3">
<summary className="cursor-pointer text-sm text-muted-foreground">Advanced</summary>
<CodexRunnerSelect value={runner ?? (state.agent ? agentRunner(state.agent.adapterType) : "auto")}
<AgentRunnerSelect value={runner ?? (state.agent ? agentRunner(state.agent.adapterType) : "auto")}
pending={adapters.isFetching} error={adapters.error?.message} onRetry={() => { void adapters.refetch(); }}
defaultRunner={codexAvailability?.defaultRunner} supportedRunners={codexAvailability?.supportedRunners}
defaultRunner={runnerAvailability?.defaultRunner} supportedRunners={runnerAvailability?.supportedRunners}
onChange={setRunner} />
</details>}
+10 -10
View File
@@ -1,7 +1,7 @@
import type { AgentRunnerChoice } from "@paperclipai/shared";
import { agentHarnessType } from "@paperclipai/shared";
import { adaptersApi } from "../api/adapters";
import { CodexRunnerSelect } from "./CodexRunnerSelect";
import { AgentRunnerSelect } from "./AgentRunnerSelect";
import { healthApi } from "@/api/health";
import { LocalProviderLoginInstructions } from "./AdapterLoginChrome";
import { useLocalAiLogin } from "./ai-connections/useLocalAiLogin";
@@ -600,7 +600,7 @@ function OnboardingWizardInner({
restoreOnboardingAdapterType(saved?.adapterType, saved?.runnerProvider),
);
const [runner, setRunner] = useState<AgentRunnerChoice>(() => saved?.runner === "legacy" || saved?.runner === "paperclip"
? saved.runner : saved?.adapterType === "paperclip_runner" && (saved?.runnerProvider === undefined || saved?.runnerProvider === "codex") ? "paperclip" : "auto");
? saved.runner : saved?.adapterType === "paperclip_runner" && (saved?.runnerProvider === undefined || ["codex", "claude", "acpx"].includes(String(saved?.runnerProvider))) ? "paperclip" : "auto");
/**
* Whether a model source has been chosen, as opposed to which one
* `adapterType` happens to hold.
@@ -629,7 +629,7 @@ function OnboardingWizardInner({
// Recover the harness and runner separately. Host command overrides from an
// old native draft cannot describe the provider; portable model/cwd survive.
const [model, setModel] = useState(
savedNativeRunnerDraft && saved?.runnerProvider !== undefined && saved.runnerProvider !== "codex" ? "" : (saved?.model as string) ?? "",
savedNativeRunnerDraft && saved?.runnerProvider !== undefined && !["codex", "claude", "acpx"].includes(String(saved.runnerProvider)) ? "" : (saved?.model as string) ?? "",
);
const [command, setCommand] = useState(
savedNativeRunnerDraft ? "" : (saved?.command as string) ?? "",
@@ -1550,7 +1550,7 @@ function OnboardingWizardInner({
setAdapterEnvResult(null);
adapterEnvResultAppliedStoredLoginRef.current = false;
setAdapterEnvError(null);
}, [step, adapterType, runner, model, command, args, url, credentialMode, apiKey, selectedSavedKey, selectedApiKey?.id, savedSubscription?.id, createdCompanyId, resolvedLoginEnvironmentId, runner === "auto" ? runnerDiscovery.data?.find(item => item.type === "codex_local")?.defaultRunner : undefined]);
}, [step, adapterType, runner, model, command, args, url, credentialMode, apiKey, selectedSavedKey, selectedApiKey?.id, savedSubscription?.id, createdCompanyId, resolvedLoginEnvironmentId, runner === "auto" ? runnerDiscovery.data?.find(item => item.type === adapterType)?.defaultRunner : undefined]);
/**
* Leaving the step puts the row back to a question.
@@ -1963,7 +1963,7 @@ function OnboardingWizardInner({
adapterType,
{
adapterConfig: adapterConfigOverride ?? buildAdapterConfig(),
...(adapterType === "codex_local" && runner !== "auto" ? { runner } : {}),
...(["codex_local", "claude_local"].includes(adapterType) && runner !== "auto" ? { runner } : {}),
...(managedBindingForStep() ? { aiConnection: managedBindingForStep() } : {}),
environmentId,
}
@@ -2212,7 +2212,7 @@ function OnboardingWizardInner({
role: agentRole,
adapterType,
adapterConfig: hireAdapterConfig,
...(adapterType === "codex_local" && runner !== "auto" ? { runner } : {}),
...(["codex_local", "claude_local"].includes(adapterType) && runner !== "auto" ? { runner } : {}),
...(shouldApplyStoredClaudeLogin ? { applyStoredClaudeLogin: true } : {}),
// The server owns what the first agent is told now: this marker seeds
// the chief-of-staff persona over the agent's entry instruction file.
@@ -2908,12 +2908,12 @@ function OnboardingWizardInner({
) : null}
</motion.div>
{sourceSelected && adapterType === "codex_local" && <details className="space-y-3">
{sourceSelected && ["codex_local", "claude_local"].includes(adapterType) && <details className="space-y-3">
<summary className="cursor-pointer text-sm text-muted-foreground">Advanced</summary>
<CodexRunnerSelect value={runner} disabled={loading || adapterEnvLoading}
<AgentRunnerSelect value={runner} disabled={loading || adapterEnvLoading}
pending={runnerDiscovery.isFetching} error={runnerDiscovery.error?.message} onRetry={() => { void runnerDiscovery.refetch(); }}
defaultRunner={runnerDiscovery.data?.find(item => item.type === "codex_local")?.defaultRunner}
supportedRunners={runnerDiscovery.data?.find(item => item.type === "codex_local")?.supportedRunners}
defaultRunner={runnerDiscovery.data?.find(item => item.type === adapterType)?.defaultRunner}
supportedRunners={runnerDiscovery.data?.find(item => item.type === adapterType)?.supportedRunners}
onChange={setRunner} />
</details>}
@@ -225,8 +225,6 @@ export function AgentBasicsDialog({
placeholder="Choose a native harness…"
onValueChange={provider => { setRunnerProvider(provider); setAdapterType("paperclip_runner"); }}
options={[
{ value: "codex", label: "Codex (Paperclip Runner)" },
{ value: "claude", label: "Claude Code (Paperclip Runner)" },
{ value: "grok", label: "Grok Build (Paperclip Runner)" },
{ value: "opencode", label: "OpenCode (Paperclip Runner)" },
]} />
+22 -22
View File
@@ -4,7 +4,7 @@ import { useAgentAppearanceDraft } from "../../hooks/useAgentAppearanceDraft";
import { AiConnectionField, aiProviderForAdapter } from "../ai-connections/AiConnectionField";
import type { AiConnectionBinding } from "@paperclipai/shared";
import type { AgentRunnerChoice } from "@paperclipai/shared";
import { CodexRunnerSelect } from "../CodexRunnerSelect";
import { AgentRunnerSelect } from "../AgentRunnerSelect";
import { SelectPopover } from "../ui/select";
import { DEFAULT_CODEX_LOCAL_MODEL } from "@paperclipai/adapter-codex-local";
import {
@@ -90,9 +90,9 @@ export function NewAgentSetup() {
key={`${selectedCompanyId}:${params.get("name")}:${params.get("adapterType")}:${params.get("runnerProvider")}:${params.get("runner")}`}
companyId={selectedCompanyId}
name={params.get("name") ?? ""}
adapterType={params.get("adapterType") === "paperclip_runner" && (params.get("runnerProvider") ?? "codex") === "codex" ? "codex_local" : params.get("adapterType") ?? ""}
initialRunner={params.get("runner") ?? (params.get("adapterType") === "paperclip_runner" && (params.get("runnerProvider") ?? "codex") === "codex" ? "paperclip" : "auto")}
preserveNativeCodex={params.get("adapterType") === "paperclip_runner" && (params.get("runnerProvider") ?? "codex") === "codex"}
adapterType={params.get("adapterType") === "paperclip_runner" && ["codex", "claude"].includes(params.get("runnerProvider") ?? "codex") ? params.get("runnerProvider") === "claude" ? "claude_local" : "codex_local" : params.get("adapterType") ?? ""}
initialRunner={params.get("runner") ?? (params.get("adapterType") === "paperclip_runner" && ["codex", "claude"].includes(params.get("runnerProvider") ?? "codex") ? "paperclip" : "auto")}
preserveNativeHarness={params.get("adapterType") === "paperclip_runner" && ["codex", "claude"].includes(params.get("runnerProvider") ?? "codex")}
runnerProvider={params.get("runnerProvider") ?? "codex"}
createdAgentId={params.get("createdAgentId")}
/>
@@ -105,7 +105,7 @@ function Setup({
adapterType,
runnerProvider,
initialRunner,
preserveNativeCodex,
preserveNativeHarness,
createdAgentId,
}: {
companyId: string;
@@ -113,7 +113,7 @@ function Setup({
adapterType: string;
runnerProvider: string;
initialRunner: string;
preserveNativeCodex: boolean;
preserveNativeHarness: boolean;
createdAgentId: string | null;
}) {
const navigate = useNavigate();
@@ -121,7 +121,7 @@ function Setup({
const { openNewIssue } = useDialogActions();
const appearanceDraft = useAgentAppearanceDraft(`${companyId}:new-agent`);
const [runner, setRunner] = useState<AgentRunnerChoice>(initialRunner === "legacy" || initialRunner === "paperclip" ? initialRunner : "auto");
const requestAdapterType = preserveNativeCodex && runner === "paperclip" ? "paperclip_runner" : adapterType;
const requestAdapterType = preserveNativeHarness && runner === "paperclip" ? "paperclip_runner" : adapterType;
const isRunner = adapterType === "paperclip_runner";
const isDot = isRunner && runnerProvider === "openai_dot";
const brandType = isDot ? "openai_dot" : isRunner
@@ -287,7 +287,7 @@ function Setup({
queryKey: queryKeys.adapters.availability(companyId, environmentId),
queryFn: () => adaptersApi.list({ companyId, environmentId }),
});
const defaultRunner = adapters.data?.find(adapter => adapter.type === "codex_local")?.defaultRunner;
const defaultRunner = adapters.data?.find(adapter => adapter.type === adapterType)?.defaultRunner;
useEffect(() => {
resetTest();
}, [companyId, environmentId, runner === "auto" ? defaultRunner : undefined]);
@@ -342,7 +342,7 @@ function Setup({
) && (requestAdapterType !== "paperclip_runner" || adapterType === "paperclip_runner" || adapterInventory.data?.some(adapter => adapter.type === "paperclip_runner" && adapter.loaded && !adapter.disabled));
const ready = Boolean(
available &&
(adapterType !== "codex_local" || runner !== "auto" || Boolean(adapters.data)) &&
(!["codex_local", "claude_local"].includes(adapterType) || runner !== "auto" || Boolean(adapters.data)) &&
!environmentError &&
!envs.isPending &&
!settings.isPending &&
@@ -383,9 +383,9 @@ function Setup({
}
: {}),
};
const config = getUIAdapter(adapterType).buildAdapterConfig(values);
if (preserveNativeCodex && runner === "paperclip") {
Object.assign(config, { provider: "codex", codexPermissionMode: "never", lifecycleMode: "per_turn" });
const config = getUIAdapter(preserveNativeHarness && runner === "paperclip" ? "paperclip_runner" : adapterType).buildAdapterConfig(preserveNativeHarness && runner === "paperclip" ? { ...values, adapterSchemaValues: runnerProvider === "claude" ? { provider: "acpx", acpxAgent: "claude", acpxPermissionMode: "approve-all" } : { provider: "codex" } } : values);
if (preserveNativeHarness && runner === "paperclip") {
Object.assign(config, runnerProvider === "claude" ? { provider: "acpx", acpxAgent: "claude", acpxPermissionMode: "approve-all", lifecycleMode: "per_turn" } : { provider: "codex", codexPermissionMode: "never", lifecycleMode: "per_turn" });
}
if (isRunner)
Object.assign(config, {
@@ -473,7 +473,7 @@ function Setup({
companyId,
adapterType: requestAdapterType,
providerAdapter: brandType,
...(adapterType === "codex_local" && runner !== "auto" ? { runner } : {}),
...(["codex_local", "claude_local"].includes(adapterType) && runner !== "auto" ? { runner } : {}),
adapterConfig: config,
testCredentials: pendingCredentials(nextConnection),
aiConnection: runtimeAiBinding ?? nextConnection?.aiConnection,
@@ -546,7 +546,7 @@ function Setup({
role: existing.length ? "general" : "ceo",
...(leader ? { reportsTo: leader.id } : {}),
adapterType: requestAdapterType,
...(adapterType === "codex_local" && runner !== "auto" ? { runner } : {}),
...(["codex_local", "claude_local"].includes(adapterType) && runner !== "auto" ? { runner } : {}),
adapterConfig: config,
defaultEnvironmentId:
environmentOverride ||
@@ -814,12 +814,12 @@ function Setup({
setScreen("runtime");
}}
/> : <p role="status" className="text-sm text-muted-foreground">Loading connection settings…</p>}
{adapterType === "codex_local" && <details className="mt-5 space-y-3">
{["codex_local", "claude_local"].includes(adapterType) && <details className="mt-5 space-y-3">
<summary className="cursor-pointer text-sm text-muted-foreground">Advanced</summary>
<CodexRunnerSelect value={runner} disabled={busy}
<AgentRunnerSelect value={runner} disabled={busy}
pending={adapters.isFetching} error={adapters.error?.message} onRetry={() => { void adapters.refetch(); }}
defaultRunner={adapters.data?.find(item => item.type === "codex_local")?.defaultRunner}
supportedRunners={adapters.data?.find(item => item.type === "codex_local")?.supportedRunners}
defaultRunner={adapters.data?.find(item => item.type === adapterType)?.defaultRunner}
supportedRunners={adapters.data?.find(item => item.type === adapterType)?.supportedRunners}
onChange={value => { setRunner(value); resetTest(); }} />
</details>}
</OnboardingCard>
@@ -1165,12 +1165,12 @@ function Setup({
</Field>
</div>
)}
{adapterType === "codex_local" && <details className="space-y-3">
{["codex_local", "claude_local"].includes(adapterType) && <details className="space-y-3">
<summary className="cursor-pointer text-sm text-muted-foreground">Advanced</summary>
<CodexRunnerSelect value={runner} disabled={busy}
<AgentRunnerSelect value={runner} disabled={busy}
pending={adapters.isFetching} error={adapters.error?.message} onRetry={() => { void adapters.refetch(); }}
defaultRunner={adapters.data?.find(item => item.type === "codex_local")?.defaultRunner}
supportedRunners={adapters.data?.find(item => item.type === "codex_local")?.supportedRunners}
defaultRunner={adapters.data?.find(item => item.type === adapterType)?.defaultRunner}
supportedRunners={adapters.data?.find(item => item.type === adapterType)?.supportedRunners}
onChange={value => { setRunner(value); resetTest(); }} />
</details>}
</section>
@@ -1,7 +1,7 @@
import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode, type Ref } from "react";
import { useQuery } from "@tanstack/react-query";
import { ArrowLeft, Check, ChevronDown, Plus, RotateCcw, Search, X, Zap } from "lucide-react";
import { aiRuntimeConnectionBindingSchema, type Agent, type IssueAssigneeAdapterOverrides } from "@paperclipai/shared";
import { aiRuntimeConnectionBindingSchema, agentHarnessType, type Agent, type IssueAssigneeAdapterOverrides } from "@paperclipai/shared";
import { agentsApi, type AdapterModel } from "@/api/agents";
import { queryKeys } from "@/lib/queryKeys";
import { cn } from "@/lib/utils";
@@ -51,7 +51,7 @@ const HARNESS_LABELS: Record<string, string> = {
function harnessLabel(agent: Agent | undefined): string {
if (!agent) return "Choose an agent";
const harness = HARNESS_LABELS[agent.adapterType] ?? agent.adapterType;
const harness = HARNESS_LABELS[agentHarnessType(agent.adapterType, agent.adapterConfig)] ?? agent.adapterType;
const provider = composerCatalogProvider(agent);
const binding = aiRuntimeConnectionBindingSchema.safeParse(agent?.runtimeConfig?.aiConnection).data;
const poolId = binding?.mode === "router" ? binding.connectionId : undefined;
@@ -3,7 +3,7 @@ import { claudeLocalReasoningEffortsForModel } from "@paperclipai/adapter-claude
import { resolvePaperclipRunnerModel } from "@paperclipai/adapter-utils";
import { DEFAULT_GROK_LOCAL_MODEL, grokLocalReasoningEffortsForModel } from "@paperclipai/adapter-grok-local";
import { DEFAULT_KIMI_LOCAL_MODEL, modelSupportsEffort, KIMI_SUPPORTED_EFFORTS } from "@paperclipai/adapter-kimi-local";
import { aiConnectionBindingSchema, aiRuntimeConnectionBindingSchema, type Agent, type IssueAssigneeAdapterOverrides } from "@paperclipai/shared";
import { aiConnectionBindingSchema, aiRuntimeConnectionBindingSchema, agentHarnessType, type Agent, type IssueAssigneeAdapterOverrides } from "@paperclipai/shared";
export interface ComposerRunSettings {
model: string | null;
@@ -65,12 +65,12 @@ export function composerEfforts(agent: Agent | undefined, model: string, catalog
: agent.adapterType === "kimi_local" ? DEFAULT_KIMI_LOCAL_MODEL : "");
if (!effectiveModel) return [];
const pool = aiRuntimeConnectionBindingSchema.safeParse(agent.runtimeConfig?.aiConnection).data?.mode === "router";
if (pool && agent.adapterType === "paperclip_runner") return isCodexLocalKnownModel(effectiveModel) ? codexLocalReasoningEffortsForModel(effectiveModel) : [];
if (pool && agent.adapterType === "paperclip_runner") return isCodexLocalKnownModel(effectiveModel) ? codexLocalReasoningEffortsForModel(effectiveModel) : /^claude-/.test(effectiveModel) ? claudeLocalReasoningEffortsForModel(effectiveModel) : [];
if (agent.adapterType === "codex_local" || (agent.adapterType === "paperclip_runner" && composerCatalogProvider(agent) === "codex")) {
return isCodexLocalKnownModel(effectiveModel) ? codexLocalReasoningEffortsForModel(effectiveModel) : [];
}
if (!catalogIds.includes(effectiveModel)) return [];
if (agent.adapterType === "claude_local") return claudeLocalReasoningEffortsForModel(effectiveModel);
if (agentHarnessType(agent.adapterType, agent.adapterConfig) === "claude_local") return claudeLocalReasoningEffortsForModel(effectiveModel);
if (agent.adapterType === "grok_local") return grokLocalReasoningEffortsForModel(effectiveModel);
if (agent.adapterType === "pi_local") return ["off", "minimal", "low", "medium", "high", "xhigh"];
if (agent.adapterType === "kimi_local" && typeof agent.adapterConfig.engine === "string"
+9 -6
View File
@@ -575,13 +575,14 @@ function importAdapterValues(
sourceAdapterType?: string,
runner?: AgentRunnerChoice,
): CreateConfigValues {
const nativeCodex = sourceAdapterType === "paperclip_runner" && choice === "codex_local";
const nativeHarness = sourceAdapterType === "paperclip_runner" && ["codex_local", "claude_local"].includes(choice);
const nativeCodex = nativeHarness && choice === "codex_local";
const savedBypass = sourceConfig?.dangerouslyBypassApprovalsAndSandbox
?? (nativeCodex ? undefined : sourceConfig?.dangerouslyBypassSandbox);
return {
...defaultCreateValues,
adapterType: choice === "openai_dot" ? "paperclip_runner" : choice,
...(runner !== undefined ? { runner } : nativeCodex ? { runner: agentRunner("paperclip_runner") } : {}),
...(runner !== undefined ? { runner } : nativeHarness ? { runner: agentRunner("paperclip_runner") } : {}),
...(sourceConfig && choice === "codex_local" ? {
model: typeof sourceConfig.model === "string" ? sourceConfig.model : "",
thinkingEffort: String(sourceConfig.reasoningEffort ?? sourceConfig.modelReasoningEffort ?? ""),
@@ -591,6 +592,7 @@ function importAdapterValues(
? (sourceConfig.codexPermissionMode ?? PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES.codex.defaultMode) === "never"
: DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
} : {}),
...(sourceConfig && choice === "claude_local" ? { model: typeof sourceConfig.model === "string" ? sourceConfig.model : "", thinkingEffort: String(sourceConfig.modelReasoningEffort ?? sourceConfig.reasoningEffort ?? sourceConfig.effort ?? "") } : {}),
...(sourceAdapterType === "paperclip_runner" && choice !== "openai_dot"
? { adapterSchemaValues: { ...sourceConfig } } : {}),
...(choice === "openai_dot" ? {
@@ -1670,14 +1672,15 @@ export function CompanyImport() {
return importPreview.manifest.agents.map((a) => {
const nativeCodex = a.adapterType === "paperclip_runner"
&& (a.adapterConfig?.provider === "codex" || a.adapterConfig?.provider === undefined);
const nativeHarness = nativeCodex || (a.adapterType === "paperclip_runner" && agentHarnessType(a.adapterType, a.adapterConfig) === "claude_local");
const adapterChoice = a.adapterType === "paperclip_runner" && a.adapterConfig?.provider === "openai_dot"
? "openai_dot" : nativeCodex ? agentHarnessType(a.adapterType, a.adapterConfig) : a.adapterType;
? "openai_dot" : nativeHarness ? agentHarnessType(a.adapterType, a.adapterConfig) : a.adapterType;
let fallbackAdapterType: string | null = null;
let availabilityWarning: string | undefined;
if (a.adapterType === "paperclip_runner") {
const available = nativeCodex ? runnerInstalled : adapterChoice === "openai_dot" ? dotAvailable : nativeRunnerAvailable;
if (!available && (availableAdapterTypes || !nativeCodex)) {
availabilityWarning = nativeCodex
const available = nativeHarness ? runnerInstalled : adapterChoice === "openai_dot" ? dotAvailable : nativeRunnerAvailable;
if (!available && (availableAdapterTypes || !nativeHarness)) {
availabilityWarning = nativeHarness
? "Paperclip Runner is unavailable here. Choose a runner or another harness explicitly."
: `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.`;
}
+12 -2
View File
@@ -412,11 +412,21 @@ describe("New agent setup", () => {
await click("Finish setup");
expect(api.hire.mock.calls[0][1].defaultEnvironmentId).toBe("grok-sandbox");
});
it.each([false, true])("keeps non-Codex runner setup links gated when the experiment is disabled (cloud=%s)", async (cloud) => {
it.each([false, true])("keeps other native setup links gated when the experiment is disabled (cloud=%s)", async (cloud) => {
cache.setQueryData(queryKeys.health, { status: "ok", cloud: { managed: cloud } });
settings.getExperimental.mockResolvedValue({ enableNativeRunner: false });
await render("paperclip_runner", "grok");
expect(container.textContent).toContain("This adapter is unavailable");
expect(api.hire).not.toHaveBeenCalled();
});
it.each([false, true])("restores native Claude setup intent and its connection flow with experiment=false (cloud=%s)", async cloud => {
cache.setQueryData(queryKeys.health, { status: "ok", cloud: { managed: cloud } });
settings.getExperimental.mockResolvedValue({ enableNativeRunner: false });
await render("paperclip_runner", "claude");
expect(container.textContent).toContain("This adapter is unavailable");
expect(container.textContent).not.toContain("This adapter is unavailable");
expect(container.textContent).toContain("Claude Code");
expect(container.textContent).toContain("Paperclip Runner");
expect(container.textContent).toContain("Connect a model");
expect(api.hire).not.toHaveBeenCalled();
});
it("blocks direct setup links for unsupported Cloud adapters", async () => {