feat(onboarding): round-4 corrections to the connect and agent steps (#12796)

Reads the connect and agent steps from the design's own values through the
Figma MCP rather than measuring an export, which corrected the arc column
inset (433px content, 64px inset — `--sz-68px` goes with the mismeasurement
it was minted for) and restored the selected tile's border alongside its
fill.

Round-4 items: sources named for the provider you sign in with, OpenAI's
mark inlined so it can take `currentColor` on a light tile, monochrome
autofill via `box-shadow` (Chrome ignores `background-color`), and the
agent step's placeholder.

Also wires `MODEL_SOURCE_NAMES`, which was added for the rename and never
read — the tiles kept passing the display registry's label, so the step
still showed "Claude Code" and "Codex" under a heading asking which
provider you are signing in to. Covered by a test that fails on the
unwired version.
This commit is contained in:
Tonio 2026-09-03 20:52:31 -07:00 committed by GitHub
parent 82ee0a68d4
commit 9ef3b087c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 121 additions and 12 deletions

View File

@ -1849,6 +1849,31 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", (
return { root, queryClient };
}
it("names the tiles for the provider, not the adapter type", async () => {
// `MODEL_SOURCE_NAMES` exists so this row says "Claude" and "OpenAI" —
// which provider you are signing in to, the question the step's heading
// asks — rather than the display registry's tool names, which the agent
// config screens want. It was added with a long comment justifying it and
// then never read, so the row went on rendering whatever the registry
// supplied: "Claude Code" and "Codex" in the app, and the bare type here,
// since this suite's registry mock returns `label: type`.
mockAdapterRegistry.list = [{ type: "claude_local" }, { type: "codex_local" }];
const { root } = await openStep4({ adapterType: "claude_local" });
const labels = [...document.body.querySelectorAll("button[aria-checked]")].map(
(tile) => tile.textContent ?? "",
);
expect(labels.length, "both recommended sources should render").toBe(2);
expect(labels.some((l) => l.includes("Claude"))).toBe(true);
expect(labels.some((l) => l.includes("OpenAI"))).toBe(true);
// The negative half is the one that fails on the unwired version: the
// registry label is the adapter type, and it must not reach the tile.
expect(labels.join(" ")).not.toContain("claude_local");
expect(labels.join(" ")).not.toContain("codex_local");
await act(async () => root.unmount());
});
it("will not advance on a saved adapter the step no longer offers", async () => {
// A draft can name an adapter this registry does not carry — a cloud
// sandbox without claude_local, an adapter since disabled. The row hides
@ -2036,7 +2061,10 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", (
// the credential switch instead. What is asserted below is unchanged —
// changing the source re-reads the signal — only the route there is.
// The tile's text is the label plus its credential tag, hence the prefix.
await clickByText((t) => t.startsWith("codex_local"));
// That label is the provider name now, not the adapter type: this row
// asks which provider you are signing in to, so it reads through
// `MODEL_SOURCE_NAMES` rather than the display registry.
await clickByText((t) => t.startsWith("OpenAI"));
expect(mockAgentsApi.getAdapterAuthSignal).toHaveBeenCalledWith(
"company-new",

View File

@ -201,7 +201,49 @@ function adapterConfigHasAnthropicApiKey(config: Record<string, unknown>): boole
*/
const MODEL_SOURCE_BRAND_MARKS: Record<string, string> = {
claude_local: "/brands/claude-color.svg",
codex_local: "/brands/codex-color.svg",
};
/**
* What the connect step calls each source.
*
* Deliberately not the display registry's label, which ten other surfaces read.
* This step asks which *provider* you are signing in with the panel under the
* row says "Sign in to Anthropic" and "Sign in to OpenAI" while the agent
* config screens name the tool that runs ("Codex CLI was not found on this
* host"). One rename in the registry would make that message say OpenAI, which
* is vaguer, not clearer.
*
* It is a tension worth naming rather than hiding: DESIGN.md asks for one name
* per concept, and this is two names for one adapter. The concepts are
* different vendor here, tool there but if the product decides otherwise,
* this map is the thing to delete.
*/
const MODEL_SOURCE_NAMES: Record<string, string> = {
claude_local: "Claude",
codex_local: "OpenAI",
};
/**
* OpenAI's blossom, inline rather than served from `/brands`.
*
* The supplied asset is a white fill, which was fine while this row only ever
* sat on a dark tile. It follows the reader's system setting now, and white on
* the light tile is invisible. Inlining lets the path take
* `currentColor` and be legible in both, which an `<img>` cannot do.
*/
function OpenAiBlossom({ className }: { className?: string }) {
return (
<svg viewBox="0 0 716 716" className={className} fill="none" aria-hidden>
<path
fill="currentColor"
d="M508.749 317.399C516.777 287.314 508.991 253.884 485.389 230.282C461.788 206.681 428.36 198.895 398.273 206.923C376.231 184.928 343.39 174.956 311.148 183.596C278.906 192.234 255.45 217.292 247.36 247.361C217.291 255.451 192.233 278.91 183.595 311.149C174.957 343.391 184.927 376.232 206.924 398.274C198.896 428.359 206.683 461.789 230.284 485.391C253.885 508.992 287.313 516.779 317.401 508.75C339.442 530.745 372.286 540.717 404.525 532.079C436.767 523.441 460.223 498.384 468.313 468.315C498.383 460.224 523.44 436.766 532.078 404.526C540.716 372.285 530.747 339.443 508.749 317.402V317.399ZM470.899 244.776C486.892 260.77 493.488 282.601 490.687 303.412L415.577 260.046C412.411 258.218 408.509 258.218 405.345 260.046L317.401 310.82V277.526C317.401 275.191 318.652 273.005 320.676 271.837L387.644 233.174C414.178 218.353 448.346 222.223 470.901 244.776H470.899ZM357.837 311.144L398.275 334.491V381.185L357.837 404.532L317.398 381.185V334.491L357.837 311.144ZM264.776 269.693C265.207 239.305 285.644 211.649 316.453 203.393C338.3 197.54 360.505 202.744 377.127 215.573L302.014 258.937C298.848 260.764 296.898 264.144 296.898 267.798V369.346L268.065 352.699C266.043 351.531 264.776 349.353 264.776 347.017V269.691V269.693ZM203.391 316.454C209.244 294.608 224.854 277.978 244.276 269.999V356.73C244.276 360.384 246.226 363.763 249.392 365.591L337.337 416.365L308.503 433.013C306.481 434.181 303.961 434.188 301.939 433.02L234.971 394.357C208.868 378.789 195.138 347.261 203.391 316.454ZM244.775 470.9C228.781 454.906 222.186 433.075 224.986 412.264L300.096 455.63C303.263 457.457 307.164 457.457 310.328 455.63L398.273 404.856V438.149C398.273 440.485 397.022 442.671 394.997 443.839L328.029 482.502C301.495 497.322 267.327 493.452 244.772 470.9H244.775ZM450.897 445.982C450.466 476.371 430.029 504.027 399.22 512.283C377.373 518.136 355.168 512.932 338.547 500.102L413.659 456.738C416.826 454.911 418.775 451.532 418.775 447.877V346.329L447.609 362.977C449.631 364.145 450.897 366.323 450.897 368.659V445.985V445.982ZM512.282 399.221C506.429 421.068 490.819 437.697 471.397 445.676V358.946C471.397 355.292 469.448 351.912 466.281 350.085L378.336 299.311L407.17 282.663C409.192 281.495 411.712 281.487 413.734 282.655L480.702 321.318C506.805 336.887 520.536 368.415 512.282 399.221Z"
/>
</svg>
);
}
const MODEL_SOURCE_INLINE_MARKS: Record<string, ComponentType<{ className?: string }>> = {
codex_local: OpenAiBlossom,
};
/**
@ -228,6 +270,8 @@ function ModelSourceMark({
type: string;
Fallback: ComponentType<{ className?: string }>;
}) {
const Inline = MODEL_SOURCE_INLINE_MARKS[type];
if (Inline) return <Inline className="size-full" />;
const brand = MODEL_SOURCE_BRAND_MARKS[type];
if (!brand) return <Fallback className="size-full" />;
return <img src={brand} alt="" className="size-full" />;
@ -2175,7 +2219,7 @@ function OnboardingWizardInner({
// tiles stretch and the name field sits under a question far
// narrower than itself.
isAgentArcStep || step === 1
? "w-(--sz-560px) max-w-full px-8 py-10 sm:px-(--sz-68px) sm:py-11"
? "w-(--sz-560px) max-w-full px-8 py-10 sm:px-(--sz-64px) sm:py-11"
: "w-full max-w-md px-8 py-12",
)}
>
@ -2644,7 +2688,7 @@ function OnboardingWizardInner({
<Input
id="onboarding-agent-name"
className="h-(--sz-44px) rounded-lg border-transparent bg-muted shadow-none dark:bg-muted"
placeholder="e.g. Chief of staff, Designer, Ron..."
placeholder="e.g. Chief of staff"
value={agentName}
onChange={(e) => setAgentName(e.target.value)}
autoFocus
@ -2675,7 +2719,18 @@ function OnboardingWizardInner({
label="Model source"
sources={recommendedAdapters.map((opt) => ({
id: opt.type,
label: opt.label,
// The vendor name where this step has one, the registry's
// tool name where it does not. `MODEL_SOURCE_NAMES` was
// added with the reasoning above it and then never read,
// so the row went on showing "Claude Code" and "Codex"
// — the tool names — under a heading asking which
// provider you are signing in to.
//
// The fallback is what keeps the row rendering if the
// registry ever marks a third adapter `recommended`:
// an unnamed source gets its tool name rather than
// nothing.
label: MODEL_SOURCE_NAMES[opt.type] ?? opt.label,
icon: <ModelSourceMark type={opt.type} Fallback={opt.icon} />,
}))}
mode={credentialMode}

View File

@ -82,16 +82,17 @@ function ModelSourceTile({
// and lending it to focus as well would mean tabbing across the row
// looked like picking every tile in turn.
"outline-none focus-visible:ring-ring/50 focus-visible:ring-(length:--rad-3)",
// Selection is a lighter surface, not a brighter edge. Both states keep
// the same border — it draws the tile, not the choice — and the fill
// carries the state. A bright stroke on one tile made the row read as
// one outlined object beside one plain one, rather than two of a kind
// with one of them picked.
// Selection is a lighter surface *and* a brighter edge. An earlier pass
// here used the fill alone, reasoning that a bright stroke on one tile
// made the row read as one outlined object beside a plain one. The
// design does both, and it is right: at these sizes one step of fill is
// too quiet to answer "which did I pick?" from across the screen, and
// the stroke is what carries it.
//
// Hover stops short of the selected fill, so pointing at a tile says
// "this one is live" rather than "this one is chosen".
selected
? "border-border bg-accent"
? "border-foreground/40 bg-accent"
: "border-border bg-card hover:bg-accent/40",
)}
>

View File

@ -446,6 +446,32 @@
}
@layer base {
/*
Autofill, in the app's own colours.
Chrome paints an autofilled field with a fixed blue-tinted fill of its own
it ignores `background-color` entirely, which is why nothing in the token
layer reached it and a filled email field came out blue on an otherwise
monochrome screen. `box-shadow` is the one property that does reach it: an
inset shadow thick enough to cover the field repaints the surface, and
`-webkit-text-fill-color` does the same job for the text.
The transition delay is the standard trick for keeping it: Chrome re-applies
its own fill on interaction, and a delay long enough to outlast the frame is
what stops it flashing back.
*/
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active,
textarea:-webkit-autofill,
select:-webkit-autofill {
-webkit-text-fill-color: var(--foreground);
caret-color: var(--foreground);
box-shadow: inset 0 0 0 1000px var(--muted);
transition: background-color 100000s ease-in-out 0s;
}
* {
@apply border-border outline-ring/50;
}
@ -2373,7 +2399,6 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] {
--sz-calc-13: calc(0.75rem + 0.5rem); /* Extracted from ui/src/components/IssueRow.tsx (ml-[calc(theme(spacing.3)+theme(spacing.2))]). */
--sz-140px: 140px; /* Extracted from ui/src/components/JsonSchemaForm.tsx (min-h-[140px]). */
--sz-52px: 52px; /* Extracted from ui/src/components/KanbanBoard.tsx (w-[52px]). */
--sz-68px: 68px; /* The onboarding arc's side inset — 560px frame, 424px column. */
--sz-48px: 48px; /* Extracted from ui/src/components/KanbanBoard.tsx (min-w-[48px]). */
--sz-260px: 260px; /* Extracted from ui/src/components/KanbanBoard.tsx (min-w-[260px]). */
--sz-120px: 120px; /* Extracted from ui/src/components/KanbanBoard.tsx (min-h-[120px]). */