mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 17:33:51 +02:00
feat(ui): enable Grok in Cloud agent setup (#13791)
## Thinking Path > - Paperclip manages AI agents and their execution settings. > - The new-agent flow selects an adapter before configuring credentials and a model. > - Cloud uses one adapter policy for the picker and direct setup links. > - That policy excludes Grok despite its existing adapter and xAI connection support. > - This change adds Grok to the Cloud policy. > - Cloud users can configure Grok with the existing subscription or API-key flow. ## Linked Issues or Issue Description **What existing behavior does this improve?** Agent creation on Cloud. **Current behavior** The Cloud picker offers Claude, Codex, and OpenCode. Direct Grok setup links also fail the shared adapter check. **Proposed behavior** Offer Grok alongside the existing choices. Use the existing xAI connection and managed sandbox setup. **Reason and benefit** Users can select an already-supported adapter through the Cloud creation flow. **Breaking changes** None. Loaded and enabled checks still apply. Other excluded adapters remain excluded. ## What Changed - Add `grok_local` to the shared Cloud creation policy. - Display four adapter choices in a 2×2 grid on desktop and mobile, and reuse the theme-aware provider mark on the connection step so Grok is visible in dark mode. - Verify picker navigation and both Grok authentication methods through sandbox setup, model testing, and agent creation. - Verify that probe and hire payloads carry the xAI connection binding without the entered API key. - Update the agent configuration specification. ## Verification - `cd ui && pnpm exec vitest run src/components/NewAgentDialog.test.tsx src/pages/NewAgent.test.tsx src/components/new-agent/AgentProviderConnection.test.tsx` — 69 tests passed. - Chromium checks against the production components and built stylesheet — four cards occupy two rows and two columns at 1280px and 390px; the connection step loads and displays the white Grok logo in dark mode and the black logo in light mode. - `pnpm --filter @paperclipai/ui typecheck` — passed. - `pnpm --filter @paperclipai/ui build` — passed. - `pnpm check:token-gates` — passed. - `git diff origin/master...HEAD | gitleaks stdin --redact --no-banner` — no leaks found; manual diff review found no private identifiers or user data. - `pnpm -r typecheck` and `pnpm build` — blocked at the existing runner package because `cargo` is not installed locally. - `pnpm test:run` — started locally, then stopped after the equivalent CI suites passed. - Initial [PR CI](https://github.com/paperclipai/paperclip/actions/runs/35681351752) passed, including full typecheck/build, general and serialized tests, Rust checks, and all eight browser-test shards. One unchanged Cursor test timed out on the first attempt; its five-test file passed locally and the failed CI shard passed on retry. - The [latest CI run](https://github.com/paperclipai/paperclip/actions/runs/35683642812) passed build, typecheck, all general and serialized tests, Rust checks, and all eight browser-test shards. One unchanged local-service-supervisor test failed its HTTP readiness check on the first attempt; its six-test file passed locally, and the failed server shard passed on retry. - Live xAI login and model execution were not run; the setup tests mock provider calls. ## Risks Small UI policy change. The existing Grok adapter, authentication, and secret storage paths remain in use. No schema or control-plane change is required. Cloud must deploy a tenant-app release containing this change. Revert the policy entry to hide Grok from new-agent setup again. ## Model Used - OpenAI GPT-6 (Codex), with repository inspection, code editing, and shell-based verification. ## 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 <noreply@paperclip.ing>
This commit is contained in:
1 parent
8725d6ce09
commit
83abfa46f6
6 files changed
+68
-13
No files matched your search
@@ -132,7 +132,7 @@ it.each([false, undefined])(
|
||||
},
|
||||
);
|
||||
|
||||
it("offers only Claude, Codex, and OpenCode on Cloud, even with the runner enabled", async () => {
|
||||
it("offers Claude, Codex, OpenCode, and Grok on Cloud, even with the runner enabled", async () => {
|
||||
await act(async () => {
|
||||
cache.setQueryData(queryKeys.health, {
|
||||
status: "ok",
|
||||
@@ -160,8 +160,15 @@ it("offers only Claude, Codex, and OpenCode on Cloud, even with the runner enabl
|
||||
[...document.querySelectorAll<HTMLInputElement>('input[type="radio"]')].map(
|
||||
(input) => input.value,
|
||||
),
|
||||
).toEqual(["claude_local", "codex_local", "opencode_local"]);
|
||||
).toEqual(["claude_local", "codex_local", "opencode_local", "grok_local"]);
|
||||
expect(document.body.textContent).not.toContain("CLI harness");
|
||||
await act(async () =>
|
||||
document.querySelector<HTMLInputElement>('input[value="grok_local"]')!.click(),
|
||||
);
|
||||
await click("Configure agent");
|
||||
const query = new URL(state.navigate.mock.calls[0][0], "http://local").searchParams;
|
||||
expect(query.get("adapterType")).toBe("grok_local");
|
||||
expect(query.get("name")).toBe("Ada & Co");
|
||||
});
|
||||
|
||||
it("keeps agent-only invitations reachable from the new-agent flow", async () => {
|
||||
|
||||
@@ -226,7 +226,7 @@ export function AgentBasicsDialog({
|
||||
{error.message}
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<div className={cn("grid grid-cols-2 gap-3", choices.length !== 4 && "sm:grid-cols-3")}>
|
||||
{choices.map((adapter) => {
|
||||
const display = getAdapterDisplay(adapter.type);
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { agentsApi } from "@/api/agents";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { AdapterLoginPanel } from "../AgentConfigForm";
|
||||
import { AdapterMark } from "./AgentBasicsDialog";
|
||||
import {
|
||||
LocalProviderLoginInstructions,
|
||||
OnboardingCardField,
|
||||
@@ -222,13 +223,7 @@ export function AgentProviderConnection({
|
||||
{
|
||||
id: adapterType,
|
||||
label: provider,
|
||||
icon: (
|
||||
<img
|
||||
src={adapterType === "grok_local" ? "/brands/adapters/grok.svg" : `/brands/${adapterType === "claude_local" ? "claude" : "codex"}-color.svg`}
|
||||
className="size-6"
|
||||
alt=""
|
||||
/>
|
||||
),
|
||||
icon: <AdapterMark type={adapterType} />,
|
||||
},
|
||||
]}
|
||||
mode={method}
|
||||
|
||||
@@ -2,6 +2,7 @@ const CLOUD_ADAPTERS = new Set([
|
||||
"claude_local",
|
||||
"codex_local",
|
||||
"opencode_local",
|
||||
"grok_local",
|
||||
]);
|
||||
|
||||
/** Creation policy shared by the picker and direct setup links. */
|
||||
|
||||
@@ -80,8 +80,8 @@ vi.mock("@/components/AgentConfigForm", () => ({
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
),
|
||||
AdapterLoginPanel: ({ onStored }: { onStored: (id: string) => void }) => (
|
||||
<button onClick={() => onStored("stored-claim")}>
|
||||
AdapterLoginPanel: ({ onConnected }: { onConnected: (id: string) => void }) => (
|
||||
<button onClick={() => onConnected("login-session")}>
|
||||
Complete subscription login
|
||||
</button>
|
||||
),
|
||||
@@ -213,6 +213,56 @@ describe("New agent setup", () => {
|
||||
expect(container.textContent).toContain("This adapter is unavailable");
|
||||
expect(api.hire).not.toHaveBeenCalled();
|
||||
});
|
||||
it.each(["subscription", "api_key"])("configures Grok on Cloud with an xAI %s connection", async (method) => {
|
||||
cache.setQueryData(queryKeys.health, {
|
||||
status: "ok",
|
||||
cloud: { managed: true },
|
||||
});
|
||||
envApi.list.mockResolvedValue([
|
||||
{ id: "sandbox-1", name: "Paperclip Cloud", driver: "sandbox", config: { provider: "daytona" } },
|
||||
]);
|
||||
envApi.capabilities.mockResolvedValue({
|
||||
sandboxProviders: { daytona: { supportsLoginPty: true } },
|
||||
});
|
||||
settings.get.mockResolvedValue({ defaultEnvironmentId: "sandbox-1" });
|
||||
settings.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: true });
|
||||
api.getAdapterAuthSignal.mockResolvedValue({ status: "missing" });
|
||||
api.testEnvironment.mockResolvedValue({ ...pass, adapterType: "grok_local" });
|
||||
await render("grok_local");
|
||||
expect(container.textContent).toContain("Connect Atlas to Grok");
|
||||
if (method === "subscription") {
|
||||
await click("GrokSubscription");
|
||||
await click("Complete subscription login");
|
||||
} else {
|
||||
await click("Use API key insteadUse subscription insteadUse API key instead");
|
||||
await click("GrokAPI");
|
||||
await fill("API key", "example-test-secret");
|
||||
await click("Connect");
|
||||
expect(managedApi.create).toHaveBeenCalledWith("company-1", expect.objectContaining({
|
||||
provider: "xai", method: "api_key", apiKey: "example-test-secret",
|
||||
}));
|
||||
}
|
||||
await fill("Model", "grok-code-fast-1");
|
||||
await click("Run test");
|
||||
const binding = { provider: "xai", method, mode: "responsible_user" };
|
||||
expect(api.testEnvironment).toHaveBeenLastCalledWith("company-1", "grok_local", expect.objectContaining({
|
||||
environmentId: "sandbox-1",
|
||||
adapterConfig: expect.objectContaining({ model: "grok-code-fast-1" }),
|
||||
aiConnection: binding,
|
||||
testCredentials: {},
|
||||
}));
|
||||
await click("Finish setup");
|
||||
expect(api.hire).toHaveBeenCalledTimes(1);
|
||||
expect(api.hire.mock.calls[0][1]).toMatchObject({
|
||||
adapterType: "grok_local",
|
||||
defaultEnvironmentId: "sandbox-1",
|
||||
adapterConfig: { model: "grok-code-fast-1" },
|
||||
runtimeConfig: { aiConnection: binding, heartbeat: { enabled: false } },
|
||||
});
|
||||
expect(JSON.stringify(api.testEnvironment.mock.calls)).not.toContain("example-test-secret");
|
||||
expect(JSON.stringify(api.hire.mock.calls)).not.toContain("example-test-secret");
|
||||
expect(container.textContent).toContain("Your agent is ready");
|
||||
});
|
||||
it("sends Cursor Cloud repo/ref and transient API key, then saves an organization secret", async () => {
|
||||
await render("cursor_cloud");
|
||||
expect(container.querySelector('[aria-label="Model"]')).toBeNull();
|
||||
|
||||
Reference in new issue
Block a user