The agent, drawn as itself (#12274)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A new customer's first session ends in the tenant's agent arc:
create an agent, connect a model, review
> - Walking it turned up questions the arc had no business asking — a
role picker using a vocabulary the customer has not been given, a model
picker asking them to judge models they have not met — and chrome
restating what they had just watched happen
> - Each one costs a first-session customer attention at the exact
moment they are deciding what this product is
> - This pull request cuts the arc to what it must ask, and draws the
agent as itself so the arc has a visible subject
> - The benefit is three steps that each ask one thing, ending on an
agent that is visibly ready

## Linked Issues or Issue Description

No public issue exists. The changes come from walking the sign-up arc
end to end.

**What happened:**
The agent step asks for a role from a fixed enum before asking for a
name. The model step shows two "Recommended" badges (on both options),
an "Adapter type" eyebrow, and a model picker. The review step lists a
three-row checklist of work the customer just performed. The progress
strip is a full-width segmented bar.

**Expected behavior:**
The agent step asks for a name. The model step offers the two harnesses
and hides the rest behind advanced settings. The review step says the
agent is ready. The strip counts three discrete steps.

**Steps to reproduce:**
1. Sign up and enter the tenant wizard on the agent arc.
2. Observe the role select above the optional name field.
3. Continue to the model step: both options carry a "Recommended" badge,
and a model picker sits below.
4. Continue to review: a checklist restates the organization name,
agent, and model.

**Additional context:**
The brand pill assets (`pill-1-dormant.svg`, `pill-1-alive.svg`) are
transcribed verbatim into a component rather than approximated. The role
removal exposed a latent silent-failure path — see Risks.

## What Changed

- `PillGuy` renders the brand pill in two states; the arc holds one
instance, dormant through create and connect, alive on review.
- The agent step asks for a name only. The name is required; the role
picker is gone.
- `DEFAULT_AGENT_ROLE` (`general`) backs every onboarding hire, and
`agentRole` now defaults to it rather than empty.
- The model step drops both "Recommended" badges, the "Adapter type"
eyebrow, and the model picker; "More Agent Adapter Types" becomes
"Advanced settings"; the sub-line becomes "Paperclip works with your
existing subscription or API keys."
- The review step drops its checklist; the heading becomes "Let's get
started..." with "[name] is ready to work!".
- The progress strip renders three left-aligned dots at the previous
gap.
- Five e2e specs and both wizard unit suites migrate off
`#onboarding-agent-role`.

## Verification

Run the tenant suite:

```
cd ui && npx vitest run
```

- 4398 tests pass across 474 files; `npx tsc --noEmit` clean.
- Walked live in a local instance: agent step (dots, dormant pill, name
placeholder), model step (no badges/eyebrow/picker, "Advanced
settings"), review (pill alive, new copy, no checklist).
- The retargeted role test asserts the hire payload carries `role:
"general"` and the typed name — it is the test that catches the silent
failure below.

## Risks

- **A latent silent failure, now closed.** `handleGiveHeartbeat` returns
early when `agentRole` is empty. With the picker removed and no default,
Connect would have hired nobody and shown no error. The default closes
it; the guard stays for any future path that clears the role.
- **Behavioral change:** every onboarding hire is filed as `general`
rather than a chosen role. The role remains editable in the app.
- **Behavioral change:** the model is no longer chosen during
onboarding. Every adapter offered here resolves its own default in
`buildAdapterConfig`, and the model is changeable later.
- **Assets:** the pill carries its own gradient fills and does not
follow the theme. That is deliberate — the agent looks like itself on
either ground.

## Model Used

Claude Opus 5 (`claude-opus-5`) via Claude Code, with tool use and code
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
- [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>
This commit is contained in:
TonioandClaude Opus 5 authored and GitHub committed 2026-08-26 21:39:55 -07:00
1 parent 24d639abad
commit eb86fcd498
13 files changed
+481 -350

No files matched your search

@@ -73,16 +73,15 @@ async function runOnboardingWizard(page: Page, companyName: string) {
if (await frontDoor.count()) await frontDoor.first().click();
// Step 1: company name.
await page.getByPlaceholder("Acme Corp").fill(companyName);
await page.getByRole("button", { name: /^Next/ }).click();
await page.getByPlaceholder("e.g. Northwind Labs").fill(companyName);
await page.getByRole("button", { name: /^Continue/ }).click();
// Step 1's "Next" creates the company; the mission step no longer runs.
// Step 3: the lead's role, then its name. The role gates "Next", and
// choosing one fills the name — so the walk only types here to override it.
await page.waitForSelector("#onboarding-agent-role", { timeout: 15_000 });
await page.locator("#onboarding-agent-role").click();
await page.getByRole("option", { name: "CEO", exact: true }).click();
// Step 3: name the agent. The role picker is gone — the arc asks for a
// name and hires under the neutral `general` role.
await page.waitForSelector("#onboarding-agent-name", { timeout: 30_000 });
await page.locator("#onboarding-agent-name").fill("Ada");
await page.getByRole("button", { name: /^Next/ }).click();
// Step 4: adapter (claude_local default); heartbeat is intercepted.
+7 -7
View File
@@ -66,15 +66,15 @@ test.describe("NUX Phase 4 visual QA", () => {
await createCard.first().click();
}
await expect(
page.getByRole("heading", { name: "Name your organization" }),
page.getByRole("heading", { name: "What is the name of your organization?" }),
).toBeVisible({ timeout: 15_000 });
await page.getByPlaceholder("Acme Corp").fill("QA Robotics");
await page.getByPlaceholder("e.g. Northwind Labs").fill("QA Robotics");
await page.screenshot({ path: shot("02-create-name.png") });
await page.getByRole("button", { name: /^Next/ }).click();
await page.getByRole("button", { name: /^Continue/ }).click();
// Step 1's "Next" creates the company and goes straight to the team lead.
// The mission screenshot that sat here is gone with the step it captured.
await page.waitForSelector("#onboarding-agent-role", {
await page.waitForSelector("#onboarding-agent-name", {
timeout: 30_000,
});
await page.screenshot({ path: shot("04-hire-team-lead.png") });
@@ -111,10 +111,10 @@ test.describe("NUX Phase 4 visual QA", () => {
await page.getByRole("button", { name: /Add agents to your org/ }).click();
// The grow path shares step 1 (company name) before its step-2 intake.
await expect(
page.getByRole("heading", { name: "Name your organization" }),
page.getByRole("heading", { name: "What is the name of your organization?" }),
).toBeVisible({ timeout: 10_000 });
await page.getByPlaceholder("Acme Corp").fill("QA Robotics Grow");
await page.getByRole("button", { name: /^Next/ }).click();
await page.getByPlaceholder("e.g. Northwind Labs").fill("QA Robotics Grow");
await page.getByRole("button", { name: /^Continue/ }).click();
await expect(
page.getByRole("heading", { name: /Tell us about your team/ }),
).toBeVisible({ timeout: 10_000 });
+4 -4
View File
@@ -53,15 +53,15 @@ test.describe("Onboarding wizard", () => {
// Step 1 — Name your organization.
await expect(
page.getByRole("heading", { name: "Name your organization" }),
page.getByRole("heading", { name: "What is the name of your organization?" }),
).toBeVisible({ timeout: 15_000 });
await page.getByPlaceholder("Acme Corp").fill(COMPANY_NAME);
await page.getByRole("button", { name: /^Next/ }).click();
await page.getByPlaceholder("e.g. Northwind Labs").fill(COMPANY_NAME);
await page.getByRole("button", { name: /^Continue/ }).click();
// Step 1's "Next" now creates the company and goes straight to the agent.
// The mission step used to sit between them and do the creating; onboarding
// no longer asks for the mission, which is collected later in the app.
await page.waitForSelector("#onboarding-agent-role", {
await page.waitForSelector("#onboarding-agent-name", {
timeout: 30_000,
});
@@ -54,24 +54,25 @@ test("captures planning mode UI for desktop and mobile", async ({ page }) => {
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 expect(page.getByRole("heading", { name: "What is the name of your organization?" })).toBeVisible({ timeout: 15_000 });
await page.locator('input[placeholder="Acme Corp"]').fill(companyName);
await page.getByRole("button", { name: /^Next/ }).click();
await page.locator('input[placeholder="e.g. Northwind Labs"]').fill(companyName);
await page.getByRole("button", { name: /^Continue/ }).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);
// The agent step asks for a name and nothing else; the name is what gates
// "Next", and the hire is filed under the neutral `general` role.
await page.waitForSelector("#onboarding-agent-name", { timeout: 30_000 });
await page.locator("#onboarding-agent-name").fill(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 });
// The review step names the agent rather than the step.
await expect(
page.getByRole("heading", { name: "Let's get started..." }),
).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).