Skip to content

Commit 384e5f6

Browse files
revert(ui): back out onboarding port (#10786) (#11067)
This reverts commit 11e5665. #10786 ported the onboarding flow from a standalone prototype and repointed `/onboarding` at the new `CloudOnboardingFlow`, deleting the existing `OnboardingWizard.tsx` in the process. The ported flow is not ready to be the shipping onboarding experience: it landed as a single large port rather than an incremental migration, it pulled `motion`, `three` and `@types/three` onto the UI dependency list for prototype visuals, and it deleted the wizard that four in-flight pull requests (#9900, #9501, #8982 and one more) were building on — those went CONFLICTING the moment the file disappeared. Rather than keep the half-migrated state on master while that is sorted out, back the port out whole and re-land it incrementally. This restores `OnboardingWizard.tsx` and the previous versions of the four e2e specs, drops the `onboarding-preview.html` Vite entry, the DesignGuide onboarding section and the `data-viz-misc` storybook story, and removes the three prototype dependencies from `ui/package.json`. This is an exact mechanical inverse of the squash commit — 41 files, +2089/-3647, no hand edits. Reverting this commit restores all 41 files byte for byte, so the port is recoverable in full when it is ready. `pnpm-lock.yaml` is deliberately not touched. #10786 never updated it; bot commit 4683f26 (#11036) added the `motion`/`three` entries afterwards, so the lockfile is now ahead of the manifest. CI owns lockfile updates (`.github/workflows/pr.yml`) and the policy job regenerates it from the changed manifest. Co-authored-by: Paperclip <noreply@paperclip.ing>
1 parent 6b7e081 commit 384e5f6

41 files changed

Lines changed: 2089 additions & 3647 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/launch.json

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@
77
"runtimeArgs": ["-c", "TMPDIR=/tmp pnpm dev"],
88
"port": 3108,
99
"autoPort": false
10-
},
11-
{
12-
"name": "ui-preview",
13-
"runtimeExecutable": "pnpm",
14-
"runtimeArgs": ["--filter", "@paperclipai/ui", "exec", "vite"],
15-
"port": 5188,
16-
"autoPort": true
1710
}
1811
]
1912
}

.claude/skills/design-guide/references/component-index.md

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -188,24 +188,6 @@ Use in property rows, comment headers, assignee displays, and anywhere a user/ag
188188
**File:** `CompanySwitcher.tsx`
189189
**Usage:** Company selector dropdown in sidebar header.
190190

191-
### AgentCapsule
192-
193-
**File:** `AgentCapsule.tsx`
194-
**Props:** `state: "slot" | "configured" | "online"`, `gradient?: 1–10`, `size?: "sm" | "md" | "lg" | {width,height}`, `glow?: "green" | "blue"`
195-
**Usage:** The brand "capsule is the agent" pill; evolves in place across onboarding steps. Fill uses `--agent-Na/Nb` gradient tokens; honors `prefers-reduced-motion`.
196-
197-
### Onboarding primitives (OnboardingCard, OnboardingHeading, Stepper, Chip, ChoiceCard, ConnectorRow)
198-
199-
**File:** `onboarding/OnboardingPrimitives.tsx`
200-
**Usage:** Presentational pieces for the full-screen onboarding flow (`onboarding/OnboardingFlow.tsx`): the 560px card frame (`--sz-560px`), display heading + lede (text-4xl), 3-segment stepper, selectable mission chips, selectable choice cards, and connector rows. Bespoke dimensions route through verbatim `--sz-*` tokens; fields inside the flow use the shared Input/Textarea/Select/Label primitives.
201-
202-
```tsx
203-
<OnboardingCard>
204-
<Stepper step={2} />
205-
<OnboardingHeading title="Create your first agent" lede="..." />
206-
</OnboardingCard>
207-
```
208-
209191
---
210192

211193
## Layout Components

tests/e2e/conference-room-typing-intro.spec.ts

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
11
import { test, expect } from "@playwright/test";
2-
import { completeCloudOnboarding, HIRING_TASK_TITLE } from "./onboarding-flow";
32

43
/**
5-
* E2E: post-onboarding launch.
4+
* E2E: post-wizard onboarding launch.
65
*
7-
* Completing the onboarding flow creates the first assigned task and lands the
8-
* user on the company dashboard. The chat intro still has unit coverage in
9-
* BoardChat tests; the onboarding handoff no longer routes there.
6+
* Completing the onboarding wizard now creates the first assigned task and
7+
* lands the user on the company dashboard. The chat intro still has unit
8+
* coverage in BoardChat tests; the wizard handoff no longer routes there.
109
*/
1110

1211
const COMPANY_NAME = `E2E-TypingIntro-${Date.now()}`;
13-
const MISSION = "Verify the dashboard launch survives the onboarding handoff.";
12+
const MISSION = "Verify the dashboard launch survives the wizard handoff.";
13+
const FIRST_TASK_TITLE = "Hire your first engineer and create a hiring plan";
1414

15-
test.describe("Dashboard launch after onboarding", () => {
15+
test.describe("Dashboard launch after onboarding wizard", () => {
1616
test("creates the first task and opens the dashboard", async ({
1717
page,
1818
baseURL,
1919
}) => {
20+
// Intercept env-test → instant pass (avoid running a real CLI check).
21+
await page.route("**/test-environment", (route) =>
22+
route.fulfill({
23+
contentType: "application/json",
24+
body: JSON.stringify({ status: "pass", checks: [] }),
25+
}),
26+
);
27+
2028
// Intercept hire → perform a REAL hire server-side with an inert http
21-
// adapter so no real agent process spawns. (The cloud flow hires with
22-
// requireEnvProbe: false, so there is no adapter-environment probe to stub.)
29+
// adapter so no real agent process spawns.
2330
await page.route("**/agent-hires", async (route) => {
2431
const req = route.request();
2532
const body = JSON.parse(req.postData() || "{}");
@@ -47,13 +54,38 @@ test.describe("Dashboard launch after onboarding", () => {
4754

4855
await page.goto("/onboarding");
4956

50-
// Welcome → company (name + mission) → agent (role picker) → first task.
51-
// "Get started" on the task step creates the task and opens the dashboard.
52-
await completeCloudOnboarding(page, {
53-
companyName: COMPANY_NAME,
54-
mission: MISSION,
55-
choice: "hiring",
57+
// Launcher card path (existing companies) — enter the wizard if the
58+
// route shows a launcher instead of opening the wizard directly.
59+
const startBtn = page.getByRole("button", { name: /Start Onboarding/i });
60+
if (await startBtn.count()) await startBtn.first().click();
61+
62+
// Step 0: front door (skipped when the wizard opens on the create path).
63+
const frontDoor = page.getByText("Build a new company");
64+
if (await frontDoor.count()) await frontDoor.first().click();
65+
66+
// Step 1: company name.
67+
await page.getByPlaceholder("Acme Corp").fill(COMPANY_NAME);
68+
await page.getByRole("button", { name: /^Next/ }).click();
69+
70+
// Step 2: mission (direct path default).
71+
await page
72+
.getByPlaceholder("What is your team trying to achieve?")
73+
.fill(MISSION);
74+
await page.getByRole("button", { name: /Confirm mission/ }).click();
75+
76+
// Step 3: lead name (prefilled) → Next.
77+
await page.waitForSelector('input[placeholder="Chief of staff"]', {
78+
timeout: 15_000,
5679
});
80+
await page.getByRole("button", { name: /^Next/ }).click();
81+
82+
// Step 4: adapter (claude_local default); heartbeat is intercepted.
83+
await page.getByRole("button", { name: /Give it a heartbeat/ }).click();
84+
85+
// Step 5: review → Get started creates the first task and opens dashboard.
86+
const getStarted = page.getByRole("button", { name: /Get started/ });
87+
await getStarted.waitFor({ timeout: 20_000 });
88+
await getStarted.click();
5789

5890
await expect(page).toHaveURL(/\/dashboard$/, { timeout: 30_000 });
5991

@@ -66,8 +98,8 @@ test.describe("Dashboard launch after onboarding", () => {
6698
const issuesRes = await page.request.get(`/api/companies/${company.id}/issues`);
6799
expect(issuesRes.ok()).toBe(true);
68100
const issues = await issuesRes.json();
69-
const firstTask = issues.find((candidate: { title: string }) => candidate.title === HIRING_TASK_TITLE);
101+
const firstTask = issues.find((candidate: { title: string }) => candidate.title === FIRST_TASK_TITLE);
70102
expect(firstTask).toBeTruthy();
71-
await expect(page.getByText(HIRING_TASK_TITLE).first()).toBeVisible({ timeout: 15_000 });
103+
await expect(page.getByText(FIRST_TASK_TITLE).first()).toBeVisible({ timeout: 15_000 });
72104
});
73105
});

tests/e2e/nux-phase4-screenshots.spec.ts

Lines changed: 77 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,47 @@ import { test, expect } from "@playwright/test";
22
import fs from "node:fs";
33
import path from "node:path";
44
import { fileURLToPath } from "node:url";
5-
import { DEFAULT_ROLE, startCloudOnboarding } from "./onboarding-flow";
65

76
const __dirname = path.dirname(fileURLToPath(import.meta.url));
87

98
/**
109
* NUX Phase 4 — visual QA screenshot capture.
1110
*
1211
* Boots a throwaway local_trusted instance (see playwright.config.ts webServer)
13-
* and captures screenshots of every integrated onboarding surface:
14-
* - Welcome screen (path picker)
15-
* - Company step (name + mission)
16-
* - Create-your-first-agent step (role picker + capsule)
17-
* - First-task step
18-
* - "Add an agent to an existing company" entry (/:prefix/onboarding)
12+
* and captures screenshots of every surface integrated by NUX Phases 1–3:
13+
* - "Build a new company" step 1 (company name) + step 2 (mission)
14+
* - Team-lead hire step (capsule wizard, PAP-125)
15+
* - Onboarding front door (path picker)
16+
* - "Add agents to your org" growth intake
1917
* - Conference Room (BoardChat) shell + composer + activity feed
2018
* - Artifacts page
2119
*
2220
* These are structural/rendering checks — LLM-dependent streaming (CEO chat
2321
* responses, hiring-plan generation) is verified separately on an LLM-backed
24-
* instance. Screenshots land in ./test-results for upload as evidence.
22+
* instance. Screenshots land in ./nux-phase4-shots for upload as evidence.
2523
*/
2624

2725
// Write under the gitignored test-results dir so re-runs leave no untracked
2826
// noise; screenshots are uploaded to the issue as QA evidence, not committed.
2927
const SHOT_DIR = path.join(__dirname, "test-results", "nux-phase4-shots");
3028

31-
const SHOTS = [
32-
"01-welcome.png",
33-
"02-company.png",
34-
"03-agent.png",
35-
"04-first-task.png",
36-
"05-add-agent.png",
37-
"06-board-chat.png",
38-
"07-artifacts.png",
39-
];
40-
4129
function shot(name: string) {
4230
fs.mkdirSync(SHOT_DIR, { recursive: true });
4331
return path.join(SHOT_DIR, name);
4432
}
4533

34+
async function openWizard(page: import("@playwright/test").Page) {
35+
await page.goto("/onboarding");
36+
const startBtn = page.getByRole("button", { name: /Start Onboarding|New Company|Add Agent/ });
37+
if (await startBtn.count()) {
38+
await startBtn.first().click();
39+
}
40+
}
41+
4642
test.describe("NUX Phase 4 visual QA", () => {
4743
test("captures every integrated surface", async ({ page }) => {
48-
// Conference Room is flag-gated default-OFF: turn the experimental flag on
49-
// for this throwaway instance before driving that surface (Section C).
44+
// New-NUX surfaces are flag-gated default-OFF (PAP-136/137/138): turn the
45+
// experimental flag on for this throwaway instance before driving them.
5046
const flagRes = await page.request.patch("/api/instance/settings/experimental", {
5147
data: { enableConferenceRoomChat: true },
5248
});
@@ -61,43 +57,36 @@ test.describe("NUX Phase 4 visual QA", () => {
6157
const baseUrl =
6258
"http://127.0.0.1:" + (process.env.PAPERCLIP_E2E_PORT ?? "3199");
6359

64-
// ── Section A: the cloud flow, step by step ───────────────────────────
65-
await page.goto("/onboarding");
66-
60+
// ── Section A: create-company path (name → mission → hire) ────────────
61+
await openWizard(page);
62+
// Front door shows when the wizard doesn't open directly on the create
63+
// path (e.g. another spec already created a company on this instance).
64+
const createCard = page.getByRole("button", { name: /Build a new company/ });
65+
if (await createCard.count()) {
66+
await createCard.first().click();
67+
}
6768
await expect(
68-
page.getByRole("heading", { name: "Welcome to Paperclip!" }),
69+
page.getByRole("heading", { name: "Name your company" }),
6970
).toBeVisible({ timeout: 15_000 });
70-
await page.screenshot({ path: shot("01-welcome.png") });
71+
await page.getByPlaceholder("Acme Corp").fill("QA Robotics");
72+
await page.screenshot({ path: shot("02-create-name.png") });
7173

72-
await startCloudOnboarding(page);
73-
74-
// Capture the company step populated but not yet submitted, then submit.
74+
await page.getByRole("button", { name: /^Next/ }).click();
7575
await expect(
76-
page.getByRole("heading", { name: "What is the name of your company or team?" }),
77-
).toBeVisible({ timeout: 15_000 });
78-
await page.locator("#onboarding-company-name").fill("QA Robotics");
76+
page.getByRole("heading", { name: "Define your mission" }),
77+
).toBeVisible({ timeout: 10_000 });
7978
await page
80-
.locator("#onboarding-mission")
79+
.getByPlaceholder("What is your team trying to achieve?")
8180
.fill("Build affordable home robots that handle household chores.");
82-
await page.screenshot({ path: shot("02-company.png") });
83-
await page.getByRole("button", { name: /^Next/ }).click();
81+
await page.screenshot({ path: shot("03-create-mission.png") });
8482

85-
// Agent step: pick a role so the capsule + preview render, then capture
86-
// before hiring.
87-
await expect(
88-
page.getByRole("heading", { name: "Create your first agent" }),
89-
).toBeVisible({ timeout: 30_000 });
90-
await page.locator("#onboarding-agent-role").click();
91-
await page.getByRole("option", { name: DEFAULT_ROLE, exact: true }).click();
92-
await page.screenshot({ path: shot("03-agent.png") });
93-
await page.getByRole("button", { name: /^Create/ }).click();
94-
95-
// First-task step: select a choice so the card's selected state is visible.
96-
await expect(
97-
page.getByRole("heading", { name: "Assign your agent a first task" }),
98-
).toBeVisible({ timeout: 30_000 });
99-
await page.getByRole("button", { name: /Create a hiring plan/ }).click();
100-
await page.screenshot({ path: shot("04-first-task.png") });
83+
// Step 2 advances via "Confirm mission" (creates the company + goal);
84+
// step 3 is the team-lead naming step of the capsule wizard.
85+
await page.getByRole("button", { name: /Confirm mission/ }).click();
86+
await page.waitForSelector('input[placeholder="Chief of staff"]', {
87+
timeout: 30_000,
88+
});
89+
await page.screenshot({ path: shot("04-hire-team-lead.png") });
10190

10291
// The company just created anchors the route-scoped sections below.
10392
const companiesRes = await page.request.get(`${baseUrl}/api/companies`);
@@ -106,23 +95,39 @@ test.describe("NUX Phase 4 visual QA", () => {
10695
const qaCompany = (Array.isArray(companies) ? companies : []).find(
10796
(c: { name: string }) => c.name === "QA Robotics",
10897
);
109-
expect(qaCompany, "onboarding should have created QA Robotics").toBeTruthy();
98+
expect(qaCompany, "wizard should have created QA Robotics").toBeTruthy();
11099
const prefix: string = qaCompany.issuePrefix;
111100

112-
// ── Section B: "add an agent to an existing company" entry ────────────
113-
// OnboardingWizardVariant renders outside <Routes> (App.tsx), so it never
114-
// sees the :companyPrefix param and the company-scoped route still opens on
115-
// the welcome screen. The real existing-company entry is the launcher card
116-
// behind it: dismiss the overlay, then "Add Agent" opens onboarding scoped
117-
// to this company, which skips company creation and starts at the agent step.
101+
// ── Section B: front door + growth intake ─────────────────────────────
118102
await page.evaluate(() => window.localStorage.clear());
119-
await page.goto(`/${prefix}/onboarding`);
120-
await page.getByRole("button", { name: "Close onboarding" }).click();
121-
await page.getByRole("button", { name: "Add Agent" }).click();
103+
await openWizard(page);
104+
// Reach the full-screen front door (step 0): either it shows directly or
105+
// "← Back to start" returns to it from the create step.
106+
if (!(await page.getByRole("heading", { name: "Welcome to Paperclip" }).count())) {
107+
await page.getByRole("button", { name: /Back to start/ }).click();
108+
}
122109
await expect(
123-
page.getByRole("heading", { name: "Create your first agent" }),
124-
).toBeVisible({ timeout: 20_000 });
125-
await page.screenshot({ path: shot("05-add-agent.png") });
110+
page.getByRole("heading", { name: "Welcome to Paperclip" }),
111+
).toBeVisible({ timeout: 10_000 });
112+
await expect(
113+
page.getByRole("heading", { name: "Build a new company" }),
114+
).toBeVisible();
115+
await expect(
116+
page.getByRole("heading", { name: "Add agents to your org" }),
117+
).toBeVisible();
118+
await page.screenshot({ path: shot("01-front-door.png") });
119+
120+
await page.getByRole("button", { name: /Add agents to your org/ }).click();
121+
// The grow path shares step 1 (company name) before its step-2 intake.
122+
await expect(
123+
page.getByRole("heading", { name: "Name your company" }),
124+
).toBeVisible({ timeout: 10_000 });
125+
await page.getByPlaceholder("Acme Corp").fill("QA Robotics Grow");
126+
await page.getByRole("button", { name: /^Next/ }).click();
127+
await expect(
128+
page.getByRole("heading", { name: /Tell us about your team/ }),
129+
).toBeVisible({ timeout: 10_000 });
130+
await page.screenshot({ path: shot("05-growth-intake.png") });
126131

127132
// ── Section C: Conference Room (BoardChat) ────────────────────────────
128133
// Visit the company dashboard first so CompanyContext selects the company
@@ -147,7 +152,15 @@ test.describe("NUX Phase 4 visual QA", () => {
147152
await page.waitForTimeout(1_000);
148153
await page.screenshot({ path: shot("07-artifacts.png") });
149154

150-
for (const f of SHOTS) {
155+
for (const f of [
156+
"01-front-door.png",
157+
"02-create-name.png",
158+
"03-create-mission.png",
159+
"04-hire-team-lead.png",
160+
"05-growth-intake.png",
161+
"06-board-chat.png",
162+
"07-artifacts.png",
163+
]) {
151164
const p = shot(f);
152165
expect(fs.existsSync(p), `missing ${f}`).toBe(true);
153166
expect(fs.statSync(p).size, `empty ${f}`).toBeGreaterThan(1_000);

0 commit comments

Comments
 (0)