Files
paperclip/tests/e2e/nux-phase4-screenshots.spec.ts
scotttong 6f9801a46b feat(ui): NUX rework behind enableConferenceRoomChat experimental flag — capsule onboarding, conference-room chat, unified composer (#8000)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The first-run experience (onboarding wizard) and the chat surfaces
(conference-room/board chat, task threads, composers) are the product's
front door — they decide whether a new operator understands "hire
agents, give them work, review results" in the first five minutes
> - Today those surfaces feel ticket-y and form-like: the wizard is a
static multi-step form that ends in an anticlimactic "Launch" screen,
the task composer and board chat behave differently from each other, and
agent-feed issue quicklooks misbehave (multiple flyouts open at once,
cards jump on hover)
> - We wanted to iterate toward a conversational, team-centric NUX — but
without risking the workflows of everyone already running Paperclip
> - This PR reworks the NUX behind a new default-OFF
`enableConferenceRoomChat` experimental flag: a capsule-motif onboarding
wizard that builds your team as you answer, a conference-room chat
surface, one shared ChatComposer across surfaces, brand-accurate status
chips, and feed-quicklook fixes — with the pre-existing UI
fork-and-frozen as `*Classic` components that flag-OFF users keep
> - The benefit is a complete, testable modern NUX that anyone can opt
into from Settings → Experimental, with zero default behavior change and
a clean path to either graduate or drop the experiment

## Linked Issues or Issue Description

No pre-existing GitHub issue — feature description per
`feature_request.yml`:

- **Problem / motivation:** Paperclip's onboarding wizard and chat
surfaces grew up as separate ticket-centric forms. New users get a
form-filling experience rather than the feeling of standing up a team;
the board chat and task threads use different composers with different
affordances; the agent feed's issue quicklook can stack multiple
popovers and shifts cards on hover.
- **Proposed solution:** A coherent NUX experiment behind one
experimental flag (`enableConferenceRoomChat`, Settings → Experimental,
default OFF): capsule onboarding wizard with an evolving team capsule,
conference-room chat, unified `ChatComposer`, team-centric copy, brand
status chips, quicklook single-flight fix. Flag-OFF users get the exact
pre-experiment UI via frozen `*Classic` forks, verified by an on/off
parity test matrix.
- **Alternatives considered:** (a) incremental unflagged restyling —
rejected: the changes interlock across surfaces and would drip risk into
every release; (b) a separate app shell / route for the new NUX —
rejected: too much divergence, the flag + classic-fork pattern keeps the
diff reviewable and reversible.
- **Roadmap alignment:** `ROADMAP.md` lists **CEO Chat** ("a
lighter-weight way to talk to leadership agents... should still resolve
to real work objects"). This experiment is groundwork in that direction
(conference-room chat resolves to issues/tasks via the same composer
used in task threads) and does not change the core task-and-comments
model.

Related PRs found in the dedup search (same area, none duplicate this
work — they target the classic wizard, which this PR intentionally
leaves intact and mergeable):

- #5385 — Coach-driven onboarding: conversational entry +
agent-companies package import
- #5378 — Onboarding wizard: reusable adapter picker + probe card
- #6636 — ui(onboarding): friendly error surface + retry for the wizard
- #7005 — fix(onboarding): explicitly await first-task wake
- #2616 — fix: restore workspace directory config in onboarding wizard

## What Changed

- **Experimental flag plumbing** — `enableConferenceRoomChat` in shared
types/validators, server instance-settings service + API, Settings →
Experimental card with explicit enable/disable copy
- **Onboarding wizard** — classic wizard forked and frozen
(`OnboardingWizardClassic`); flag-ON variant is a 5-step capsule wizard
with a persistent evolving `AgentCapsule` (gradient/glow motif),
team-centric reframed copy, and a typing-dots intro (hardened with
fake-timer tests)
- **Conference-room chat** — flag-ON board-chat surface with agent
bubble name/icon headers and copy/vote/timestamp action rows
(`AgentBubbleActionRow`)
- **Unified composer** — shared `ChatComposer` adopted across surfaces;
translucent surface + scroll-mask removal; "Agent mode"/"Plan mode"
relabels; no-assignee confirmation `AlertDialog` (new
`ui/alert-dialog.tsx` primitive); `@task` reference picker +
linkification in mentions
- **Agent feed** — single-flight issue-quicklook store (one popover at a
time), flyouts open to the left, removed hover translate-y jitter
- **Status chips** — brand-accurate task status chips behind the flag
(light/dark, 1px borders per paperclip.ing/brand)
- **Tests** — flag on/off parity matrix across IssueDetail,
NewIssueDialog, Sidebar, wizard, gate components; component tests for
all new pieces
- **Merge with `master`** — one conflict in
`ui/src/components/IssueChatThread.tsx`, resolved by keeping master's
new `AssigneeChip`/`HandoffWakeRow`/`RunStatusBadge` components inside
the flag-gated metadata-row chrome (details in commit `21a5642a`);
post-merge fixes: vitest 4 mock typing in `MarkdownEditor.test.tsx`,
flag hook made safe for provider-less mounts (master's new isolated
component tests)
- **Branch hygiene** — internal design wireframes/mockups stripped
before the PR (they live in the Paperclip issue threads)
- No user-facing documentation changes required: the flag is
intentionally experimental and self-described in the Settings card; no
existing docs reference the affected surfaces

## Verification

- `pnpm run typecheck` — green across the workspace (ui, server, shared,
plugins)
- Full UI suite (`vitest run` in `ui/`, clean worktree at this HEAD):
**1593/1595 passing, 223/224 files** — the 2 remaining failures are in
`src/components/artifacts/ArtifactCard.test.tsx` and **fail identically
on pristine `origin/master`** (pre-existing upstream, unrelated to this
branch)
- Full server suite (`vitest run` in `server/`, same clean worktree):
results in PR checks; flag plumbing covered by instance-settings tests
- Targeted post-merge resolution check: `IssueChatThread`,
`IssueChatThreadSystemNotice`, `IssueDetail`, `Sidebar`,
`ConferenceRoomChatGate`, `OnboardingWizardVariant`, `NewIssueDialog`,
`InstanceExperimentalSettings`, `MarkdownEditor` — 172/172 passing
- Manual walkthrough: flag OFF (default) → onboarding wizard, task
thread, board chat, composer all render the classic UI; flag ON via
Settings → Experimental → capsule wizard, conference-room chat, unified
composer, status chips active
- Screenshots: see below

**Flag on/off screenshots** (committed on this branch under
`screenshots/PR-8000-*`):

| Surface | Flag OFF (classic, default) | Flag ON (experimental) |
| --- | --- | --- |
| Settings → Experimental | ![settings
off](https://raw.githubusercontent.com/paperclipai/paperclip/9b4f02708383031d3124b68ca3ed9ab437ea3501/screenshots/PR-8000-settings-experimental-flag-off.png)
| ![settings
on](https://raw.githubusercontent.com/paperclipai/paperclip/9b4f02708383031d3124b68ca3ed9ab437ea3501/screenshots/PR-8000-settings-experimental-flag-on.png)
|
| Task thread | ![thread
off](https://raw.githubusercontent.com/paperclipai/paperclip/9b4f02708383031d3124b68ca3ed9ab437ea3501/screenshots/PR-8000-task-thread-flag-off.png)
| ![thread
on](https://raw.githubusercontent.com/paperclipai/paperclip/9b4f02708383031d3124b68ca3ed9ab437ea3501/screenshots/PR-8000-task-thread-flag-on.png)
|
| Home / nav | ![home
off](https://raw.githubusercontent.com/paperclipai/paperclip/9b4f02708383031d3124b68ca3ed9ab437ea3501/screenshots/PR-8000-home-flag-off.png)
| ![home
on](https://raw.githubusercontent.com/paperclipai/paperclip/9b4f02708383031d3124b68ca3ed9ab437ea3501/screenshots/PR-8000-home-flag-on.png)
|
| Conference Room (flag-ON only surface) | — | ![conference
room](https://raw.githubusercontent.com/paperclipai/paperclip/9b4f02708383031d3124b68ca3ed9ab437ea3501/screenshots/PR-8000-conference-room-flag-on.png)
|

Capsule onboarding wizard walkthrough screenshots (flag ON) are attached
to the Paperclip design/implementation threads; the wizard requires a
fresh instance so it is captured via the e2e harness
(`tests/e2e/nux-phase4-screenshots.spec.ts`).


## Risks

- **Large surface, but gated:** all new behavior sits behind
`enableConferenceRoomChat`, default OFF; flag-OFF rendering is locked by
frozen `*Classic` forks plus an on/off parity test suite
- **Classic forks are frozen at the fork point (`e3aada1d`):** master
features added to the live thread component after that point (assignee
handoff chips, run status badge, composer mention coach) render in the
flag-ON path; the flag-OFF task thread keeps the fork-point behavior
until the experiment graduates (forks deleted) or is dropped (forks
restored as canonical). Called out for reviewer attention.
- **Merge-conflict resolution in `IssueChatThread.tsx`** (commit
`21a5642a`) deserves reviewer eyes: master's new handoff/run-status
components were kept; the base toast-style no-assignee flow remains
replaced by the AlertDialog flow introduced on this branch
- Schema/server changes are additive (one optional boolean instance
setting); no migrations of existing data

## Model Used

- Claude (Anthropic) via Claude Code running in the Paperclip agent
harness (agent: ClaudeCoder)
- Branch implemented across multiple agent sessions on Claude Opus-class
models with extended thinking + tool use (file edits, shell, Playwright
screenshots); merge/PR session model ID as reported by the harness:
`claude-fable-5` (Claude Code CLI)
- All code was agent-authored and board-reviewed through Paperclip issue
threads (plans, wireframes, confirmations) before merging

## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes (none
required — experimental flag, self-documenting Settings card; noted
above)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green (run 3 on `8af3041a`: all 16
gates SUCCESS, incl. e2e and all 4 serialized-suite shards)
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(re-review verdict: Confidence 5/5, “Safe to merge”; all 4 round-1
findings fixed + confirmed resolved; both summary notes addressed in
`8af3041a`)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:32:55 -05:00

176 lines
7.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* NUX Phase 4 — visual QA screenshot capture.
*
* Boots a throwaway local_trusted instance (see playwright.config.ts webServer)
* and captures screenshots of every surface integrated by NUX Phases 13:
* - "Build a new team" step 1 (team name) + step 2 (mission)
* - Team-lead hire step (capsule wizard, PAP-125)
* - Onboarding front door (path picker)
* - "Add agents to your org" growth intake
* - Conference Room (BoardChat) shell + composer + activity feed
* - Artifacts page
*
* These are structural/rendering checks — LLM-dependent streaming (CEO chat
* responses, hiring-plan generation) is verified separately on an LLM-backed
* instance. Screenshots land in ./nux-phase4-shots for upload as evidence.
*/
// Write under the gitignored test-results dir so re-runs leave no untracked
// noise; screenshots are uploaded to the issue as QA evidence, not committed.
const SHOT_DIR = path.join(__dirname, "test-results", "nux-phase4-shots");
function shot(name: string) {
fs.mkdirSync(SHOT_DIR, { recursive: true });
return path.join(SHOT_DIR, name);
}
async function openWizard(page: import("@playwright/test").Page) {
await page.goto("/onboarding");
const startBtn = page.getByRole("button", { name: /Start Onboarding|New Company|Add Agent/ });
if (await startBtn.count()) {
await startBtn.first().click();
}
}
test.describe("NUX Phase 4 visual QA", () => {
test("captures every integrated surface", async ({ page }) => {
// New-NUX surfaces are flag-gated default-OFF (PAP-136/137/138): turn the
// experimental flag on for this throwaway instance before driving them.
const flagRes = await page.request.patch("/api/instance/settings/experimental", {
data: { enableConferenceRoomChat: true },
});
expect(flagRes.ok()).toBe(true);
const consoleErrors: string[] = [];
page.on("console", (msg) => {
if (msg.type() === "error") consoleErrors.push(msg.text());
});
page.on("pageerror", (err) => consoleErrors.push("PAGEERROR: " + err.message));
const baseUrl =
"http://127.0.0.1:" + (process.env.PAPERCLIP_E2E_PORT ?? "3199");
// ── Section A: create-team path (name → mission → hire) ───────────────
await openWizard(page);
// Front door shows when the wizard doesn't open directly on the create
// path (e.g. another spec already created a company on this instance).
const createCard = page.getByRole("button", { name: /Build a new team/ });
if (await createCard.count()) {
await createCard.first().click();
}
await expect(
page.getByRole("heading", { name: "Name your team" }),
).toBeVisible({ timeout: 15_000 });
await page.getByPlaceholder("Acme Corp").fill("QA Robotics");
await page.screenshot({ path: shot("02-create-name.png") });
await page.getByRole("button", { name: /^Next/ }).click();
await expect(
page.getByRole("heading", { name: "Define your mission" }),
).toBeVisible({ timeout: 10_000 });
await page
.getByPlaceholder("What is your team trying to achieve?")
.fill("Build affordable home robots that handle household chores.");
await page.screenshot({ path: shot("03-create-mission.png") });
// Step 2 advances via "Confirm mission" (creates the company + goal);
// step 3 is the team-lead naming step of the capsule wizard.
await page.getByRole("button", { name: /Confirm mission/ }).click();
await page.waitForSelector('input[placeholder="Chief of staff"]', {
timeout: 30_000,
});
await page.screenshot({ path: shot("04-hire-team-lead.png") });
// The company just created anchors the route-scoped sections below.
const companiesRes = await page.request.get(`${baseUrl}/api/companies`);
expect(companiesRes.ok()).toBe(true);
const companies = await companiesRes.json();
const qaCompany = (Array.isArray(companies) ? companies : []).find(
(c: { name: string }) => c.name === "QA Robotics",
);
expect(qaCompany, "wizard should have created QA Robotics").toBeTruthy();
const prefix: string = qaCompany.issuePrefix;
// ── Section B: front door + growth intake ─────────────────────────────
await page.evaluate(() => window.localStorage.clear());
await openWizard(page);
// Reach the full-screen front door (step 0): either it shows directly or
// "← Back to start" returns to it from the create step.
if (!(await page.getByRole("heading", { name: "Welcome to Paperclip" }).count())) {
await page.getByRole("button", { name: /Back to start/ }).click();
}
await expect(
page.getByRole("heading", { name: "Welcome to Paperclip" }),
).toBeVisible({ timeout: 10_000 });
await expect(
page.getByRole("heading", { name: "Build a new team" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Add agents to your org" }),
).toBeVisible();
await page.screenshot({ path: shot("01-front-door.png") });
await page.getByRole("button", { name: /Add agents to your org/ }).click();
// The grow path shares step 1 (team name) before its step-2 intake.
await expect(
page.getByRole("heading", { name: "Name your team" }),
).toBeVisible({ timeout: 10_000 });
await page.getByPlaceholder("Acme Corp").fill("QA Robotics Grow");
await page.getByRole("button", { name: /^Next/ }).click();
await expect(
page.getByRole("heading", { name: /Tell us about your team/ }),
).toBeVisible({ timeout: 10_000 });
await page.screenshot({ path: shot("05-growth-intake.png") });
// ── Section C: Conference Room (BoardChat) ────────────────────────────
// Visit the company dashboard first so CompanyContext selects the company
// from the route before we land on the board-chat surface.
await page.evaluate(() => window.localStorage.clear());
await page.goto(`/${prefix}/dashboard`);
await page.waitForLoadState("networkidle");
await page.goto(`/${prefix}/board-chat`);
await expect(page).toHaveURL(new RegExp(`/${prefix}/board-chat`));
// Composer renders once a company is selected. (Regression guard for the
// Rules-of-Hooks crash that previously blanked this page — see PAP-50.)
await expect(
page.getByPlaceholder("Ask anything about your company..."),
).toBeVisible({ timeout: 20_000 });
await page.waitForTimeout(2_000); // let welcome bubble + suggestion chips stage in
await page.screenshot({ path: shot("06-board-chat.png") });
// ── Section D: Artifacts ──────────────────────────────────────────────
await page.goto(`/${prefix}/artifacts`);
await expect(page).toHaveURL(new RegExp(`/${prefix}/artifacts`));
await page.waitForLoadState("networkidle");
await page.waitForTimeout(1_000);
await page.screenshot({ path: shot("07-artifacts.png") });
for (const f of [
"01-front-door.png",
"02-create-name.png",
"03-create-mission.png",
"04-hire-team-lead.png",
"05-growth-intake.png",
"06-board-chat.png",
"07-artifacts.png",
]) {
const p = shot(f);
expect(fs.existsSync(p), `missing ${f}`).toBe(true);
expect(fs.statSync(p).size, `empty ${f}`).toBeGreaterThan(1_000);
}
// No React Rules-of-Hooks / render crashes on any surface we visited.
const hookErrors = consoleErrors.filter(
(e) => /Rendered more hooks|change in the order of Hooks/i.test(e),
);
expect(hookErrors, hookErrors.join("\n")).toHaveLength(0);
});
});