mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - New customers arrive through an onboarding arc that spans Paperclip Cloud and the tenant app > - Cloud's naming screen stopped asking for the company mission, but the tenant wizard still decided its first step by asking whether the company had one > - Every Cloud-created company therefore looked mission-less on arrival, so every walk detoured through a "Define your mission" screen the design had already removed > - This pull request removes that step from the arc, and makes step 1 create the company itself > - The benefit is a shorter arc that matches the design, and a three-step progress strip that now counts the steps that exist ## Linked Issues or Issue Description No public issue exists. The problem was found by walking staging end to end. **What happened:** A new customer signs in, names their organization, and waits for it to build. The tenant wizard then asks "Define your mission" before it asks for the first agent. Cloud no longer collects a mission, so this screen appears for every new customer. **Expected behavior:** The wizard asks for the first agent, the model, and a review. The progress strip counts three steps. **Actual behavior:** The wizard asks for the mission first. The progress strip counts five segments, because the run does not enter on the agent arc. **Additional context:** Three merged pull requests built the mission-based step choice this change removes: #11352, #11416 and #11429. The mission is now collected later, inside the tenant app, so onboarding does not ask for it at all. ## What Changed - `onboardingStepForCompany` always returns the agent step. The `companyHasMission` parameter is removed, because it cannot change the answer. - `resolveRouteOnboardingOptions` no longer accepts `companyHasMission`. - The dashboard no longer waits for the goal lookup before it opens the wizard. That wait only chose a step, and the step is now fixed. - Step 1 creates the company in a new `handleCreateCompany`. Company creation used to sit at the end of `handleConfirmMission`. - No company goal is written during onboarding. - The three-step strip now shows on the agent, model and review steps, because every Cloud-first run enters on the arc. - The full-length bar drops its second segment. No run can fill it. - The grow path keeps its step 2 questionnaire. Only the create path skips ahead. - Back from the agent step goes to the screen the run came from. - Four end-to-end specs no longer drive the wizard through the mission step. ## Verification Run the tenant test suite: ``` cd ui && npx vitest run ``` - 4356 tests pass. 471 files pass. - `npx tsc --noEmit` reports no errors. - Fault injection: forcing `skipsMissionStep` to `true` fails the grow questionnaire test. Removing the Back rule fails the Back test. Both tests fail on the exact defect they guard. - The three-step strip is asserted by an existing test. It checks `Step 1 of 3` and `aria-label="Create your first agent"`. Manual check on staging after the paired Cloud change: 1. Open a new incognito window. 2. Sign in with a new account. 3. Name the organization. 4. Confirm the wizard shows "Create your first agent" and "Step 1 of 3". ## Risks - **Behavioral change.** Onboarding no longer writes a company goal. An agent hired during onboarding starts without a seeded mission. This is intended. The mission moves to the tenant app. - **Dead code.** `ONBOARDING_MISSION_STEP` and the mission screen stay in the codebase, but nothing in the app opens them. They wait for the surface that collects the mission later. - **Grow path.** The grow path is unchanged, but it shares step 2 with the removed screen. New tests cover it. - **Superseded work.** #11352, #11416 and #11429 tuned the mission-based step choice. This change removes the branch they tuned. ## Model Used Claude Opus 5 (`claude-opus-5`), extended thinking, with tool use and code execution through Claude Code. ## 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 - [x] My branch name describes the change and contains no internal Paperclip ticket id - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] 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: Claude Opus 5 <noreply@anthropic.com>
166 lines
6.5 KiB
TypeScript
166 lines
6.5 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import {
|
|
expectLandsOnFirstTaskWithoutDashboardBounce,
|
|
instrumentNavLog,
|
|
} from "./helpers/onboarding-landing";
|
|
|
|
/** The name the CEO role fills in — see AGENT_ROLE_LABELS. */
|
|
const AGENT_NAME = "CEO";
|
|
const TASK_TITLE = "Paperclip onboarding";
|
|
|
|
test("captures planning mode UI for desktop and mobile", async ({ page }) => {
|
|
const timestamp = Date.now();
|
|
const companyName = `PAP-3413-${timestamp}`;
|
|
const screenshotDir = "test-results/planning-mode";
|
|
|
|
await instrumentNavLog(page);
|
|
|
|
await page.route("**/test-environment", (route) =>
|
|
route.fulfill({
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ status: "pass", checks: [] }),
|
|
}),
|
|
);
|
|
|
|
await page.route("**/agent-hires", async (route) => {
|
|
const req = route.request();
|
|
const body = JSON.parse(req.postData() || "{}");
|
|
const auth = req.headers().authorization;
|
|
const real = await fetch(new URL(req.url()).toString(), {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(auth ? { Authorization: auth } : {}),
|
|
},
|
|
body: JSON.stringify({
|
|
name: body.name,
|
|
role: body.role,
|
|
adapterType: "http",
|
|
adapterConfig: { url: "http://127.0.0.1:1/dead" },
|
|
runtimeConfig: { heartbeat: { enabled: false } },
|
|
}),
|
|
});
|
|
await route.fulfill({
|
|
status: real.status,
|
|
contentType: "application/json",
|
|
body: await real.text(),
|
|
});
|
|
});
|
|
|
|
await page.goto("/onboarding");
|
|
const startBtn = page.getByRole("button", { name: /Start Onboarding|New Company|Add Agent/ });
|
|
if (await startBtn.count()) await startBtn.first().click();
|
|
|
|
const createCard = page.getByRole("button", { name: /Build a new company/ });
|
|
if (await createCard.count()) await createCard.first().click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Name your organization" })).toBeVisible({ timeout: 15_000 });
|
|
|
|
await page.locator('input[placeholder="Acme Corp"]').fill(companyName);
|
|
await page.getByRole("button", { name: /^Next/ }).click();
|
|
|
|
// Naming the company creates it and goes straight to the agent step.
|
|
|
|
// The lead is no longer pre-named. Choosing a role fills the name from the
|
|
// role's label, which is also what gates "Next".
|
|
await page.waitForSelector("#onboarding-agent-role", { timeout: 30_000 });
|
|
await page.locator("#onboarding-agent-role").click();
|
|
await page.getByRole("option", { name: "CEO", exact: true }).click();
|
|
await expect(page.locator("#onboarding-agent-name")).toHaveValue(AGENT_NAME);
|
|
|
|
await page.getByRole("button", { name: /^Next/ }).click();
|
|
await page.getByRole("button", { name: /^Connect$/ }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Review" })).toBeVisible({ timeout: 30_000 });
|
|
await page.getByRole("button", { name: /Get started/ }).click();
|
|
// The wizard now drops the user straight onto the first task's detail page,
|
|
// and must not bounce through the dashboard (PAP-404).
|
|
await expectLandsOnFirstTaskWithoutDashboardBounce(page);
|
|
|
|
const baseOrigin = new URL(page.url()).origin;
|
|
const companyRes = await page.request.get(`${baseOrigin}/api/companies`);
|
|
expect(companyRes.ok()).toBe(true);
|
|
const companies = await companyRes.json();
|
|
const company = companies.find((c: { name: string }) => c.name === companyName);
|
|
expect(company).toBeTruthy();
|
|
const issueRes = await page.request.get(`${baseOrigin}/api/companies/${company.id}/issues`);
|
|
expect(issueRes.ok()).toBe(true);
|
|
const issues = await issueRes.json();
|
|
const planningSeedIssue = issues.find(
|
|
(candidate: { id: string; identifier?: string; title: string }) =>
|
|
candidate.title === TASK_TITLE,
|
|
);
|
|
expect(planningSeedIssue).toBeTruthy();
|
|
|
|
const issue = planningSeedIssue;
|
|
const issueIdentifier = issue.identifier ?? issue.id;
|
|
const issuePath = `/${company.issuePrefix ?? company.id}/issues/${issueIdentifier}`;
|
|
const companyPrefix = company.issuePrefix ?? company.id;
|
|
const issueLinkSelector = `a[href$="/issues/${issueIdentifier}"]`;
|
|
|
|
const setMode = async (mode: "standard" | "planning") => {
|
|
const patchRes = await page.request.patch(`${baseOrigin}/api/issues/${issue.id}`, {
|
|
data: { workMode: mode },
|
|
});
|
|
expect(patchRes.ok()).toBe(true);
|
|
await expect
|
|
.poll(async () => {
|
|
const currentRes = await page.request.get(`${baseOrigin}/api/issues/${issue.id}`);
|
|
expect(currentRes.ok()).toBe(true);
|
|
const current = await currentRes.json();
|
|
return current.workMode;
|
|
}, { timeout: 10_000 })
|
|
.toBe(mode);
|
|
};
|
|
|
|
await setMode("planning");
|
|
|
|
await page.goto(issuePath);
|
|
await expect(page.getByText("Plan mode").first()).toBeVisible();
|
|
const desktopPlanningToggle = page.getByTestId("task-chat-composer-mode");
|
|
await expect(desktopPlanningToggle).toBeVisible();
|
|
await expect(desktopPlanningToggle).toHaveAttribute("data-pending-work-mode", "planning");
|
|
|
|
await page.screenshot({
|
|
path: `${screenshotDir}/desktop-planning-detail-${timestamp}.png`,
|
|
fullPage: true,
|
|
});
|
|
|
|
await page.goto(`/${companyPrefix}/issues`);
|
|
await expect(page.locator(issueLinkSelector)).toBeVisible();
|
|
await expect(page.locator(issueLinkSelector)).not.toContainText("Plan mode");
|
|
await page.screenshot({
|
|
path: `${screenshotDir}/desktop-planning-row-${timestamp}.png`,
|
|
fullPage: true,
|
|
});
|
|
|
|
await page.goto(issuePath);
|
|
await page.getByTestId("task-chat-composer-mode").click();
|
|
await page.getByRole("menuitem", { name: /Auto mode/ }).click();
|
|
await expect(page.getByTestId("task-chat-composer-mode")).toHaveAttribute("data-pending-work-mode", "standard");
|
|
await page.screenshot({
|
|
path: `${screenshotDir}/desktop-standard-toggle-${timestamp}.png`,
|
|
fullPage: true,
|
|
});
|
|
|
|
await setMode("planning");
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.goto(issuePath);
|
|
await expect(page.getByText("Plan mode").first()).toBeVisible();
|
|
const mobilePlanningToggle = page.getByTestId("task-chat-composer-mode");
|
|
await expect(mobilePlanningToggle).toBeVisible();
|
|
await expect(mobilePlanningToggle).toHaveAttribute("data-pending-work-mode", "planning");
|
|
await page.screenshot({
|
|
path: `${screenshotDir}/mobile-planning-detail-${timestamp}.png`,
|
|
fullPage: true,
|
|
});
|
|
|
|
await page.goto(`/${companyPrefix}/issues`);
|
|
await expect(page.locator(issueLinkSelector)).toBeVisible();
|
|
await expect(page.locator(issueLinkSelector)).not.toContainText("Plan mode");
|
|
await page.screenshot({
|
|
path: `${screenshotDir}/mobile-planning-row-${timestamp}.png`,
|
|
fullPage: true,
|
|
});
|
|
});
|