Files
PaperClipAI/tests/e2e/planning-mode-visual-verification.spec.ts
T
DottaandPaperclip 841742fc1a [codex] Graduate experimental conference room defaults (#8628)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI has been graduating experimental conference-room task
experiences into the default issue and onboarding flows
> - Several task UI improvements were still coupled to the Conference
Room Chat experimental flag even though they are useful outside chat
itself
> - That coupling meant disabling chat also reverted unrelated defaults
such as work-mode labels, task status colors, team creation copy, and
the graduated issue thread
> - This pull request keeps chat-specific gating scoped to chat while
making the graduated task UI the default experience
> - The benefit is that operators can use the newer task workflows
without needing to enable the separate chat experiment

## Linked Issues or Issue Description

No public GitHub issue exists for this exact change.

### Problem or motivation

The Conference Room Chat experimental flag was controlling unrelated
task UI defaults, which made non-chat workflows regress when chat was
disabled.

### Proposed solution

Remove that flag from task-thread, work-mode, onboarding, status, and
team-creation presentation paths while leaving chat-specific behavior
separately gated.

### Alternatives considered

Keeping the flag as a broad umbrella until chat graduates would avoid a
behavior change, but it keeps unrelated UI improvements hidden behind
the wrong capability switch.

### Roadmap alignment

Checked `ROADMAP.md`; this is focused graduation/polish for existing UI
surfaces rather than a new roadmap-level core feature.

Related search:

- Searched open PRs and issues for `conference room chat experimental
flag`: no matches.
- Searched open PRs and issues for `graduated issue thread`: no matches.

## What Changed

- Removes Conference Room Chat flag branching from task-thread
rendering, work-mode labels, task status colors, and team creation copy.
- Makes the onboarding completion path create/reuse an onboarding
project, create the first assigned task, and send the user to the
dashboard instead of chat.
- Deletes the legacy onboarding wizard and classic task-thread files now
that the graduated flow is the default.
- Updates focused UI tests and affected E2E specs for the default task
experience.
- Adds a user-visible onboarding error if restored state is missing the
company or agent required for launch.

## Verification

- `pnpm run preflight:workspace-links && pnpm exec vitest run
ui/src/components/NewIssueDialog.test.tsx
ui/src/components/OnboardingWizardVariant.test.tsx
ui/src/components/RunChatSurface.test.tsx
ui/src/components/SidebarCompanyMenu.test.tsx
ui/src/components/StatusBadge.test.tsx ui/src/lib/agent-order.test.ts
ui/src/lib/onboarding-launch.test.ts ui/src/lib/work-mode-meta.test.ts
ui/src/pages/IssueDetail.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `npx playwright test --config tests/e2e/playwright.config.ts --list
tests/e2e/conference-room-typing-intro.spec.ts
tests/e2e/planning-mode-visual-verification.spec.ts`
- Attempted targeted Playwright execution locally, but this host is
missing Chromium system libraries (`libatk1.0-0t64`, `libatspi2.0-0t64`,
`libxcomposite1`, `libxdamage1`, `libxfixes3`, `libxrandr2`, `libgbm1`,
`libasound2t64`). CI runs the specs in the proper Actions environment.

## Risks

- Medium UI behavior risk: this intentionally changes the default
experience for users who have not enabled Conference Room Chat.
- Medium onboarding risk: completion now creates/reuses a project and
creates the first task instead of only navigating.
- Low migration risk: no database schema or migration changes are
included.
- The PR avoids `pnpm-lock.yaml` and `.github/workflows` changes.

## Model Used

OpenAI Codex, GPT-5-class coding model, tool-enabled local repository
workflow with shell, git, and test execution.

## 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>
2026-06-25 11:52:36 -05:00

162 lines
6.8 KiB
TypeScript

import { expect, test } from "@playwright/test";
const AGENT_NAME = "Chief of staff";
const TASK_TITLE = "Hire your first engineer and create a hiring plan";
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 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 team/ });
if (await createCard.count()) await createCard.first().click();
await expect(page.getByRole("heading", { name: "Name your team" })).toBeVisible({ timeout: 15_000 });
await page.locator('input[placeholder="Acme Corp"]').fill(companyName);
await page.getByRole("button", { name: /^Next/ }).click();
await expect(page.getByRole("heading", { name: "Define your mission" })).toBeVisible({ timeout: 30_000 });
await page
.getByPlaceholder("What is your team trying to achieve?")
.fill("Capture planning mode visual evidence for the graduated task UI.");
await page.getByRole("button", { name: /Confirm mission/ }).click();
await page.waitForSelector('input[placeholder="Chief of staff"]', { timeout: 30_000 });
await expect(page.locator('input[placeholder="Chief of staff"]')).toHaveValue(AGENT_NAME);
await page.getByRole("button", { name: /^Next/ }).click();
await page.getByRole("button", { name: /Give it a heartbeat/ }).click();
await expect(page.getByRole("heading", { name: "Review" })).toBeVisible({ timeout: 30_000 });
await page.getByRole("button", { name: /Get started/ }).click();
await expect(page).toHaveURL(/\/dashboard$/, { timeout: 30_000 });
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();
await expect(page.getByTestId("issue-chat-composer")).toHaveAttribute("data-pending-work-mode", "planning");
const desktopPlanningToggle = page.getByTestId("issue-chat-composer-work-mode-toggle");
await expect(desktopPlanningToggle).toBeVisible();
await expect(desktopPlanningToggle).toHaveAttribute("data-pending-work-mode", "planning");
await expect(desktopPlanningToggle).toHaveAttribute("aria-pressed", "true");
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("issue-chat-composer-work-mode-toggle").click();
await page.getByTestId("issue-chat-composer-work-mode-menu-standard").click();
await expect(page.getByTestId("issue-chat-composer")).toHaveAttribute("data-pending-work-mode", "standard");
await expect(page.getByTestId("issue-chat-composer-work-mode-toggle")).toHaveAttribute("data-pending-work-mode", "standard");
await expect(page.getByTestId("issue-chat-composer-work-mode-toggle")).toHaveAttribute("aria-pressed", "false");
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("issue-chat-composer-work-mode-toggle");
await expect(mobilePlanningToggle).toBeVisible();
await expect(mobilePlanningToggle).toHaveAttribute("data-pending-work-mode", "planning");
await expect(mobilePlanningToggle).toHaveAttribute("aria-pressed", "true");
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,
});
});