mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Operators create agents and configure their runtimes in the board UI. > - The old creation flow presents several choices and a large form before an agent can start. > - The existing onboarding controls already provide clear provider connection steps. > - This pull request uses those controls in a new-agent wizard and organizes the full configuration pages. > - Operators can connect, test, save, and assign a first task while keeping the existing configuration tools. ## Linked Issues or Issue Description Related: #10974. That earlier open PR also reorganizes agent configuration. This PR follows the reviewed Storybook designs for agent creation and the current configuration tabs. **What existing behavior does this improve?** Agent creation, provider connection, runtime tests, and full agent configuration. **Current behavior** The creation dialog leads to a large manual configuration form. Provider login controls differ from onboarding. Environment variables and secret access appear in separate places. **Proposed behavior** Choose a name and adapter. Connect Claude or Codex through the existing onboarding controls. Configure and test the runtime, save the agent, and open a task dialog with that agent assigned. Use the same design on the existing configuration tabs. **Reason and benefit** The first setup asks for fewer decisions. The full editor keeps instructions, skills, runtime controls, secret access, permissions, keys, and revisions available in clear sections. **Breaking changes** The board creation and configuration layouts change. The test-environment API adds an optional, allowlisted `testCredentials` field for one-shot probes. Database contracts stay the same. Native ACPX tests now reject unsupported local platforms before a CLI login can mask the runtime restriction. ## What Changed - Added a new-agent wizard with numbered steps, adapter branding, provider connections, editable model choices, runtime tests, and confirmation. - Added Codex app-server, Claude ACPX, and OpenCode runner choices. - Stored API credentials through existing secret APIs and persisted references in agent configuration. New setup keys are isolated from credentials used by existing agents. - Preserved external-agent invitations beside the wizard, including optional messages, one-time prompts, and clipboard fallback. - Added OpenRouter provider and secret bindings for Pi and OpenCode. - Added adapter-specific prerequisite fields for Cursor, Gemini, Kimi, and Hermes. Cursor Cloud keys are saved as new organization secrets. - Fixed Cursor Cloud repository field mapping, omitted empty remote environment values, and added useful model and repository error messages. - Preserved complete MCP assignments when multiple valid profiles contain more than 250 tools in total. Generated profiles retain exact tool selectors. - Added service branding and deployment-aware adapter choices. Cloud setup offers Claude, Codex, and OpenCode; local native runners require the experimental setting. - Made the agent list responsive at intermediate widths. - Applied the reviewed design to the real agent configuration pages. Kept the instruction editor, skills, and existing mutations. - Combined secret access and environment variables under one Save and Discard action. - Added interactive Storybook screens for setup, configuration, confirmation, authentication, and test results. - Fixed Pi provider-error parsing and thinking-effort persistence. Native ACPX validates Linux x64 on the actual local, SSH, or sandbox target. - Redacted the complete transient probe-credential field from HTTP error logs, including rejected provider names. ## Verification - Current head `df0292fe6` has a fresh Greptile 5/5 review with no unresolved findings. All 31 executed CI checks passed, including the aggregate verification gate and all browser E2E shards. Storybook visual regression is skipped by its workflow; the local Storybook build passed. - Browser tests completed real assigned tasks with direct Codex, Claude, OpenCode, Pi, and native Codex. - Verified external-agent invitation generation and automatic prompt copying in the live browser. - Pi and OpenCode used an existing OpenRouter secret. Browser checks covered save and reload, instruction edits, skill selection, environment-variable Save and Discard, and assigned task creation. - Invalid Claude API credentials remained on the connection step with an error. A live Pi/OpenRouter invalid-key probe returned a provider failure and left the user-secret inventory unchanged (zero entries before and after). - Full workspace typecheck and build passed after rebasing onto current master. After review fixes, server and UI typechecks, token gates, and the full build passed again. Storybook built successfully. - All 5,542 local UI tests passed. The Cursor Cloud and Pi adapter regressions passed all 24 tests. Review regressions passed 69 server tests and all 18 agent-list tests. - The local full test command ran 6,971 general server tests successfully. Editing review fixes during that long run caused nine tests to use stale modules; fresh isolated runs passed. An unrelated embedded-Postgres fixture hit the host shared-memory limit; its 15 affected tests passed when the fixture groups ran separately. - Local workspace groups passed after rerunning 18 CLI tests sequentially to avoid host database limits and parallel-load timeouts. The local full command stopped at the general server phase, so serialized server verification comes from the five passing CI shards. - Browser testing at 390px confirmed that the agent action menu opens and the page has no horizontal overflow. CI browser E2E shards passed. - Review the `Onboarding / New agent` and `Agents / Configuration refresh` Storybook groups. In the real app, create an agent, run its connection test, save it, assign a task, and reload its configuration. ## Risks - This changes the main agent setup and configuration UI. Regression tests cover routing, persistence, secret bindings, and form actions. - Native Claude ACPX requires Linux x64. Direct Claude works on macOS. Remote checks execute a bounded platform probe and reject unsupported or unverified targets. - A native OpenCode task reached the provider context limit because of its tool payload. Its provider connection test passed. Direct OpenCode completed a task. This existing native execution limit is not fixed here. - Claude and Codex connection keys use the existing user-secret store. Other runtime setup keys use distinct organization secrets. Existing credentials are never rotated. Probes do not store entered keys. Failed agent creation removes newly staged credentials. - Cursor Cloud has not completed a live task. Its authenticated account still needs GitHub repository access. The live run passed MCP provisioning, remote environment validation, and explicit Auto model selection before the repository prerequisite blocked execution. - Generated runtime MCP profiles can exceed the public profile-edit request limit. They still contain exact catalog selectors and preserve permission boundaries. - No database migration, dependency, lockfile, or workflow changes are included. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, repository tools, shell execution, and browser automation. The runtime did not expose the exact model ID or 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 <noreply@paperclip.ing>
193 lines
7.1 KiB
TypeScript
193 lines
7.1 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
import { NewAgentDialog } from "./NewAgentDialog";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
vi.mock("@/api/instanceSettings", () => ({
|
|
instanceSettingsApi: {
|
|
getExperimental: async () => ({ enableNativeRunner: true }),
|
|
},
|
|
}));
|
|
const invites = vi.hoisted(() => ({ createCompanyInvite: vi.fn(), getInviteOnboarding: vi.fn(), copy: vi.fn() }));
|
|
vi.mock("../api/access", () => ({ accessApi: invites }));
|
|
vi.mock("../lib/clipboard", () => ({ copyTextToClipboard: invites.copy }));
|
|
vi.mock("../context/CompanyContext", () => ({ useCompany: () => ({ selectedCompanyId: "company-1" }) }));
|
|
const state = vi.hoisted(() => ({
|
|
adapters: [] as object[],
|
|
navigate: vi.fn(),
|
|
close: vi.fn(),
|
|
}));
|
|
vi.mock("@/lib/router", () => ({ useNavigate: () => state.navigate }));
|
|
vi.mock("../context/DialogContext", () => ({
|
|
useDialog: () => ({ newAgentOpen: true, closeNewAgent: state.close }),
|
|
}));
|
|
vi.mock("@/api/adapters", () => ({
|
|
adaptersApi: { list: async () => state.adapters },
|
|
}));
|
|
vi.mock("./onboarding/PillGuy", () => ({ PillGuy: () => null }));
|
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
let root: Root;
|
|
let container: HTMLDivElement;
|
|
let cache: QueryClient;
|
|
async function click(label: string) {
|
|
const button = [...document.querySelectorAll("button")].find(
|
|
(b) => b.textContent?.trim() === label,
|
|
);
|
|
expect(button).toBeTruthy();
|
|
await act(async () => button!.click());
|
|
}
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
invites.createCompanyInvite.mockResolvedValue({ token: "one-time-token", onboardingTextPath: "/api/invites/one-time-token/onboarding.txt" });
|
|
invites.getInviteOnboarding.mockResolvedValue({ onboarding: { connectivity: {} } });
|
|
invites.copy.mockResolvedValue(undefined);
|
|
state.adapters = [
|
|
{ type: "codex_local", loaded: true },
|
|
{ type: "paperclip_runner", loaded: true },
|
|
{ type: "claude_local", loaded: true, disabled: true },
|
|
];
|
|
container = document.createElement("div");
|
|
document.body.append(container);
|
|
root = createRoot(container);
|
|
cache = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
await act(async () =>
|
|
root.render(
|
|
<QueryClientProvider client={cache}>
|
|
<NewAgentDialog />
|
|
</QueryClientProvider>,
|
|
),
|
|
);
|
|
await act(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
});
|
|
});
|
|
afterEach(async () => {
|
|
await act(async () => root.unmount());
|
|
cache.clear();
|
|
container.remove();
|
|
});
|
|
async function name() {
|
|
const input = document.querySelector("input")!;
|
|
await act(async () => {
|
|
Object.getOwnPropertyDescriptor(
|
|
HTMLInputElement.prototype,
|
|
"value",
|
|
)!.set!.call(input, "Ada & Co");
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
});
|
|
await click("Choose adapter");
|
|
}
|
|
it("requires a name and an enabled adapter before navigating", async () => {
|
|
await click("Choose adapter");
|
|
expect(document.body.textContent).toContain("Agent name");
|
|
await name();
|
|
expect(document.querySelector('input[value="claude_local"]')).toBeNull();
|
|
await click("Configure agent");
|
|
expect(state.navigate).not.toHaveBeenCalled();
|
|
await act(async () =>
|
|
(
|
|
document.querySelector('input[value="codex_local"]') as HTMLInputElement
|
|
).click(),
|
|
);
|
|
await click("Configure agent");
|
|
expect(state.close).toHaveBeenCalledTimes(1);
|
|
const query = new URL(state.navigate.mock.calls[0][0], "http://local")
|
|
.searchParams;
|
|
expect(query.get("name")).toBe("Ada & Co");
|
|
expect(query.get("adapterType")).toBe("codex_local");
|
|
});
|
|
it("offers native Codex, Claude ACPX, and OpenCode runners", async () => {
|
|
await name();
|
|
await act(async () =>
|
|
(
|
|
document.querySelector(
|
|
'input[value="paperclip_runner"]',
|
|
) as HTMLInputElement
|
|
).click(),
|
|
);
|
|
const options = [...document.querySelectorAll("option")].map(
|
|
(option) => option.textContent,
|
|
);
|
|
expect(options).toContain("Codex (app server)");
|
|
expect(options).toContain("Claude (ACPX)");
|
|
expect(options).toContain("OpenCode");
|
|
expect(options.join(" ")).not.toContain("ACPX Codex");
|
|
});
|
|
|
|
it.each([false, undefined])(
|
|
"hides the runner unless explicitly enabled (%s)",
|
|
async (enableNativeRunner) => {
|
|
await act(async () => {
|
|
cache.setQueryData(queryKeys.instance.experimentalSettings, {
|
|
enableNativeRunner,
|
|
});
|
|
});
|
|
await name();
|
|
expect(
|
|
document.querySelector('input[value="paperclip_runner"]'),
|
|
).toBeNull();
|
|
expect(document.querySelector('input[value="codex_local"]')).not.toBeNull();
|
|
},
|
|
);
|
|
|
|
it("offers only Claude, Codex, and OpenCode on Cloud, even with the runner enabled", async () => {
|
|
await act(async () => {
|
|
cache.setQueryData(queryKeys.health, {
|
|
status: "ok",
|
|
cloud: { managed: true },
|
|
});
|
|
cache.setQueryData(
|
|
queryKeys.adapters.all,
|
|
[
|
|
"claude_local",
|
|
"codex_local",
|
|
"opencode_local",
|
|
"cursor",
|
|
"cursor_cloud",
|
|
"gemini_local",
|
|
"grok_local",
|
|
"kimi_local",
|
|
"pi_local",
|
|
"hermes_local",
|
|
"paperclip_runner",
|
|
].map((type) => ({ type, loaded: true })),
|
|
);
|
|
});
|
|
await name();
|
|
expect(
|
|
[...document.querySelectorAll<HTMLInputElement>('input[type="radio"]')].map(
|
|
(input) => input.value,
|
|
),
|
|
).toEqual(["claude_local", "codex_local", "opencode_local"]);
|
|
expect(document.body.textContent).not.toContain("CLI harness");
|
|
});
|
|
|
|
it("keeps agent-only invitations reachable from the new-agent flow", async () => {
|
|
await click("Invite an external agent");
|
|
const message = document.querySelector("textarea")!;
|
|
await act(async () => {
|
|
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")!.set!.call(message, "Help with research");
|
|
message.dispatchEvent(new Event("input", { bubbles: true }));
|
|
});
|
|
await click("Generate onboarding prompt");
|
|
await act(async () => { await new Promise(resolve => setTimeout(resolve, 20)); });
|
|
expect(invites.createCompanyInvite).toHaveBeenCalledWith("company-1", {
|
|
allowedJoinTypes: "agent", humanRole: null, agentMessage: "Help with research",
|
|
});
|
|
expect(document.querySelector<HTMLTextAreaElement>('textarea[readonly]')?.value).toContain("/api/invites/one-time-token/onboarding.txt");
|
|
expect(invites.copy).toHaveBeenCalled();
|
|
expect(state.navigate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps the generated invitation readable when clipboard access fails", async () => {
|
|
invites.copy.mockRejectedValue(new Error("Clipboard unavailable"));
|
|
invites.getInviteOnboarding.mockRejectedValue(new Error("Manifest unavailable"));
|
|
await click("Invite an external agent");
|
|
await click("Generate onboarding prompt");
|
|
await act(async () => { await new Promise(resolve => setTimeout(resolve, 20)); });
|
|
expect(document.body.textContent).toContain("Copy the prompt manually");
|
|
expect(document.querySelector<HTMLTextAreaElement>('textarea[readonly]')?.value).toContain("/api/invites/one-time-token/onboarding.txt");
|
|
});
|