Repository navigation
feat(cli-authorize): the CLI sign-in page on the shared auth card - #3311
Conversation
…e auth card family Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3
/cli/authorize now draws the d215 s-cli-authorize frames: the auth card family's brand row, title, subtitle and field; the device code in mono 18px; the approve state's facts as a bordered list, the caution as a notice row with the info glyph (role="note"), and Deny / Authorize as ghost and ink keys on one row. Done, denied, expired and used are a title plus a status row; error is an alert row. The lettered "C" tile and the CLI SIGN-IN eyebrow are gone. Signed out, the read-only code sits above a "Sign in to continue" key that navigates to /v2/login?next=… with the code. Logic is unchanged. The page mounts outside V2App (body has modern-ui, not v2-canvas), so V2CliAuthorize.css restates the input and key properties App.css's legacy rules set, at higher specificity; everything else comes from .v2-login in v2.css, which is also the page's scroller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3
…pp.css The page mounts outside V2App, so App.css's legacy input, button and form rules reached it. Those rules are already gated on body:not(.v2-canvas); the page now claims v2-canvas while mounted (and index.tsx stamps it at boot for this path), so the restated key and input overrides go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Authorization rendering and navigation frontend/src/index.tsx, frontend/src/v2/components/V2CliAuthorize.tsx, frontend/src/v2/__tests__/V2CliAuthorize.test.tsx |
The boot-path condition now includes /cli/authorize, with or without a trailing slash. The page uses shared V2 login cards and a navigation button for sign-in. Tests cover canvas-class lifecycle, navigation, loading, and authorization outcomes. |
Authorization card styling and layout frontend/src/v2/components/V2CliAuthorize.css, docs/design/artboards/d215/index.html, frontend/src/v2/__tests__/v2-layout-invariants.test.ts, frontend/src/v2/__tests__/signalAccent.test.ts |
The page adds styling for the code field, hostname, request facts, actions, and notice. The design mockups show code entry, approval, and success states. Layout and style tests check shared login rules and CSS constraints. |
Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Feature
Possibly related issues
Team-Commonly/commonly#1296— This change implements the browser-based CLI authorization step, but does not implement token auto-refresh.
Possibly related PRs
- Team-Commonly/commonly#3063 — Changes the same authorization stylesheet and layout and accent tests.
- Team-Commonly/commonly#3297 — Changes shared
.v2-loginlayout rules that the updated layout tests assert. - Team-Commonly/commonly#3292 — Changes shared
.v2-logincard and mobile layout rules that the updated layout tests assert.
Merge Risk: 
Minimal · up to cdae9
-
No merge-blocking issue was identified in the authorization page changes.
Pre-merge checks: 
4
Finishing Touches
-
Generate docstrings
-
- Commit to this branch
- Create a new PR
Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
lilyshen0722
left a comment
There was a problem hiding this comment.
Code gate, PR #3311 at 47264e6: /cli/authorize moved onto the shared v2 auth card
Findings
Blocking
None.
Non-blocking
- "Sign in to continue" is now a button that calls
navigate(), where it used to be a<Link>. Seefrontend/src/v2/components/V2CliAuthorize.tsx:142-148. The destination URL is the same, and the test proves it (V2CliAuthorize.test.tsx, signed-out test). The cost is that the control is no longer a link: it has no href, middle-click and open-in-new-tab stop working, and a screen reader announces "button" for what is a navigation. The cause is the shared selector.v2-root button.v2-login__submit(v2.css:2125), which only styles a<button>. Two fixes would work: let the shared rule accept:where(a, button), as the OAuth buttons already do, or keep the button. The test now asserts that no link exists (queryByRole('link') … toBeNull()). That reverses an old assertion. The PR body cites the d215 frames for the visual change, but it doesn't say the link became a button. - The boot stamp matches the exact path only.
frontend/src/index.tsx:23checksbootPath === '/cli/authorize'. nginx (frontend/nginx.conf:59,cli/authorize/?$) and the router also serve/cli/authorize/, and that variant flashes dark until the component's effect runs. Low impact: the backendverifyUrl(backend/routes/auth.ts:267) has no trailing slash. - The body-class behaviour is checked against the source text, not by a test that renders it.
signalAccent.test.ts:125-128regex-matchesclassList.add/removeand the index.tsx condition. No test renders the page and checks thatdocument.bodyhasv2-canvaswhile mounted and loses it after unmount (or keeps it across a hand-off to a sibling that also claims it). It's worth adding; the class is what keeps App.css's legacy input and button rules off this page. - Live regions are nested. The card is
aria-live="polite"(V2CliAuthorize.tsx:115), and it now containsrole="status"/role="alert"rows (V2AuthNotice) plus arole="status"loading line (:130). Some screen readers will read the outcome twice. The old page had the same polite wrapper, so this isn't a regression, but now is a cheap time to drop the wrapper's aria-live, since each outcome row carries its own role. - The artboard repeats a rule.
docs/design/artboards/d215/index.html:717re-adds.authcard .sub {…}, which is identical to line 708. Harmless, but delete it. - The loading state has no h1 (
:130). That's unchanged from main, and the new test pins it on purpose. Noted only because the brief asked for one h1 per state; every other state has exactly one.
What I verified
1. The device flow behaves as it does on main. I compared origin/main's V2CliAuthorize.tsx with the head line by line.
- Both POSTs are byte-identical:
/api/auth/device/authorizewith{ userCode: code }for the lookup and{ userCode: code, decision }for the decision (:70-72,:95-98). normalizeCode,errorScreen(410 → expired, anything else → error),requestAgeandcurrentInstanceare unchanged.- The
?code=prefill (useMemoplususeEffect(() => setCode(queryCode))) is unchanged. - The submit guard (
code.replace('-', '').length !== 8) is unchanged. nextis built the same way. The signed-out path goes to/v2/login?next=${encodeURIComponent(next)}, and the test asserts the router actually lands on/v2/login?next=%2Fcli%2Fauthorize%3Fcode%3DABCD-EFGH.- The status → screen mapping is unchanged:
- pending with a request → confirm
- denied → denied
- consumed or authorized → used
- anything else → expired
- decision: authorized → done, anything else → denied
- The render order is the same as the old JSX conditionals: loading, then signed out, then the screens. Confirm without a request renders the empty card, as before.
- Because
card()returns the samemain > div > divshape in every state, React keeps the same DOM nodes across state changes.
2. The v2-canvas lifecycle.
- App.css gates its legacy img, form, input and button rules on
body.modern-ui:not(.v2-canvas)(App.css:20, 129, 147, 159, 165-182). V2App adds the class on mount and removes it on unmount (V2App.tsx:213-215). V2CliAuthorize now does the same (:58-63). - /cli/authorize → /v2/login, through the only navigation the page offers. Both are lazy route elements under one
<Routes>. In React 18, the passive-effect flush runs every passive unmount, deletions included, before any passive mount in the same commit (commitPassiveUnmountEffectsbeforecommitPassiveMountEffects). So V2CliAuthorize's remove runs first and V2App's add runs after it, and the class ends up present. - If V2App's chunk isn't loaded yet, a non-transition update (react-router 6, no
v7_startTransition) re-suspends a boundary that's already visible. React then hides the old tree rather than deleting it, so the remove waits for the same commit that mounts V2App. In the worst case there's a fallback-only gap, which is what main already had: main never set the class on this page at all. - /v2/login → /cli/authorize, the post-sign-in
nextreturn, works the same way in reverse. - StrictMode's double effect (add, remove, add) ends with the class present.
- Navigating to a legacy route correctly strips the class. The page has no other links: V2AuthBrand isn't a link.
- The interaction with
/'s boot-only stamp is the existing V2App limitation (documented in V2OAuthComplete.tsx:53). This PR doesn't introduce it. v2.cssis imported globally in App.tsx:23, so the.v2-login*rules load on a cold/cli/authorize.
3. CSS.
V2CliAuthorize.csshas no hex, rgb or named colour and novar(--x, fallback). Every token it uses (--v2-font-mono,--v2-r-10,--v2-border-soft,--v2-text-primary,--v2-text-secondary) is defined in v2.css.- The shared classes are used the way v2.css defines them:
.v2-loginscroller (flex:1, min-height:0, overflow-y:auto) →.v2-login__card.v2-login__card--stack(grid, a direct childformset todisplay: contents) →__title,__subtitle,__field/__label/__input,__submitand__submit--ghost,__notice. - The short-viewport scroll holds:
.v2-rootis 100vh with overflow hidden and is a flex column,.v2-loginscrolls, and the card centres withmargin: auto. - Phone width: the 480px media rule sets
.v2-login__input { font-size: 16px }at specificity (0,1,0). The code input's rule.v2-cli-authorize .v2-login__input.v2-cli-authorize__codeis (0,3,0) and setsfont: 600 18px/1 var(--v2-font-mono), so mono 18px holds at every width.
4. Accessibility.
- Each state renders exactly one h1 (loading has none, as on main).
- There is a polite live region, and outcomes get
role=status/role=alertthrough V2AuthNotice (status rows mount empty and fill a frame later, so they get announced). - The input is wrapped in a
<label>with visible text and also hasaria-label="Device code". - Continue, Deny, Authorize and Sign in to continue are all
<button>s with explicittype. - The caution is
role="note"with an info glyph, so it doesn't rely on colour.
5. Tests and the rest of the repo.
CI=true npx jest V2CliAuthorize.test.tsx signalAccent.test.ts v2-layout-invariants.test.ts(Node 22): 3 suites and 377 tests pass. The only console output is react-router's future-flag warnings.npx tsc --noEmit -p .(backend lines filtered out): clean.- The component tests assert rendered output: headings by role and name, input value and disabled state, the dt order of the facts, the dd text, the note and status/alert text including the
<code>commonly login</code>, button order and parent, and the real router landing on/v2/login?next=…. They also cover the classes. New coverage: denied, error (alert) and loading. git grepforv2-cli-authorize__card|__primary|__secondary|__field|__mark|__eyebrow|__warning|__actions, "CLI SIGN-IN", "Authorize Commonly CLI" and "Sign in to continue" across frontend, e2e/, docs, cli and backend finds no stale consumers. The only hits are the new artboard and an unrelated i18n string. e2e/ has no/cli/authorizespec. The cli and backend tests reference only the URL, which is unchanged.- The removed signalAccent assertions (warning-ink token,
__primaryoverrides, accent fill) and the reversed "CLI SIGN-IN" assertion are covered by the d215 frames cited in the PR body.
GATE: PASS
…stamp takes a trailing slash Also drops a duplicated .authcard .sub rule from the d215 artboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3
lilyshen0722
left a comment
There was a problem hiding this comment.
Code gate, PR #3311: /cli/authorize moved onto the shared v2 auth card. First reviewed at 47264e6, re-stamped at cdae9b7 (see "Re-stamp" at the end).
Findings
Blocking
None.
Non-blocking
- "Sign in to continue" is now a button that calls
navigate(), where it used to be a<Link>. Seefrontend/src/v2/components/V2CliAuthorize.tsx:142-148. The destination URL is the same, and the test proves it (V2CliAuthorize.test.tsx, signed-out test). The cost is that the control is no longer a link: it has no href, middle-click and open-in-new-tab stop working, and a screen reader announces "button" for what is a navigation. The cause is the shared selector.v2-root button.v2-login__submit(v2.css:2125), which only styles a<button>. Two fixes would work: let the shared rule accept:where(a, button), as the OAuth buttons already do, or keep the button. The test now asserts that no link exists (queryByRole('link') … toBeNull()). That reverses an old assertion. The PR body cites the d215 frames for the visual change, but it doesn't say the link became a button. - The boot stamp matches the exact path only.
frontend/src/index.tsx:23checksbootPath === '/cli/authorize'. nginx (frontend/nginx.conf:59,cli/authorize/?$) and the router also serve/cli/authorize/, and that variant flashes dark until the component's effect runs. Low impact: the backendverifyUrl(backend/routes/auth.ts:267) has no trailing slash. - The body-class behaviour is checked against the source text, not by a test that renders it.
signalAccent.test.ts:125-128regex-matchesclassList.add/removeand the index.tsx condition. No test renders the page and checks thatdocument.bodyhasv2-canvaswhile mounted and loses it after unmount (or keeps it across a hand-off to a sibling that also claims it). It's worth adding; the class is what keeps App.css's legacy input and button rules off this page. - Live regions are nested. The card is
aria-live="polite"(V2CliAuthorize.tsx:115), and it now containsrole="status"/role="alert"rows (V2AuthNotice) plus arole="status"loading line (:130). Some screen readers will read the outcome twice. The old page had the same polite wrapper, so this isn't a regression, but now is a cheap time to drop the wrapper's aria-live, since each outcome row carries its own role. - The artboard repeats a rule.
docs/design/artboards/d215/index.html:717re-adds.authcard .sub {…}, which is identical to line 708. Harmless, but delete it. - The loading state has no h1 (
:130). That's unchanged from main, and the new test pins it on purpose. Noted only because the brief asked for one h1 per state; every other state has exactly one.
What I verified
1. The device flow behaves as it does on main. I compared origin/main's V2CliAuthorize.tsx with the head line by line.
- Both POSTs are byte-identical:
/api/auth/device/authorizewith{ userCode: code }for the lookup and{ userCode: code, decision }for the decision (:70-72,:95-98). normalizeCode,errorScreen(410 → expired, anything else → error),requestAgeandcurrentInstanceare unchanged.- The
?code=prefill (useMemoplususeEffect(() => setCode(queryCode))) is unchanged. - The submit guard (
code.replace('-', '').length !== 8) is unchanged. nextis built the same way. The signed-out path goes to/v2/login?next=${encodeURIComponent(next)}, and the test asserts the router actually lands on/v2/login?next=%2Fcli%2Fauthorize%3Fcode%3DABCD-EFGH.- The status → screen mapping is unchanged:
- pending with a request → confirm
- denied → denied
- consumed or authorized → used
- anything else → expired
- decision: authorized → done, anything else → denied
- The render order is the same as the old JSX conditionals: loading, then signed out, then the screens. Confirm without a request renders the empty card, as before.
- Because
card()returns the samemain > div > divshape in every state, React keeps the same DOM nodes across state changes.
2. The v2-canvas lifecycle.
- App.css gates its legacy img, form, input and button rules on
body.modern-ui:not(.v2-canvas)(App.css:20, 129, 147, 159, 165-182). V2App adds the class on mount and removes it on unmount (V2App.tsx:213-215). V2CliAuthorize now does the same (:58-63). - /cli/authorize → /v2/login, through the only navigation the page offers. Both are lazy route elements under one
<Routes>. In React 18, the passive-effect flush runs every passive unmount, deletions included, before any passive mount in the same commit (commitPassiveUnmountEffectsbeforecommitPassiveMountEffects). So V2CliAuthorize's remove runs first and V2App's add runs after it, and the class ends up present. - If V2App's chunk isn't loaded yet, a non-transition update (react-router 6, no
v7_startTransition) re-suspends a boundary that's already visible. React then hides the old tree rather than deleting it, so the remove waits for the same commit that mounts V2App. In the worst case there's a fallback-only gap, which is what main already had: main never set the class on this page at all. - /v2/login → /cli/authorize, the post-sign-in
nextreturn, works the same way in reverse. - StrictMode's double effect (add, remove, add) ends with the class present.
- Navigating to a legacy route correctly strips the class. The page has no other links: V2AuthBrand isn't a link.
- The interaction with
/'s boot-only stamp is the existing V2App limitation (documented in V2OAuthComplete.tsx:53). This PR doesn't introduce it. v2.cssis imported globally in App.tsx:23, so the.v2-login*rules load on a cold/cli/authorize.
3. CSS.
V2CliAuthorize.csshas no hex, rgb or named colour and novar(--x, fallback). Every token it uses (--v2-font-mono,--v2-r-10,--v2-border-soft,--v2-text-primary,--v2-text-secondary) is defined in v2.css.- The shared classes are used the way v2.css defines them:
.v2-loginscroller (flex:1, min-height:0, overflow-y:auto) →.v2-login__card.v2-login__card--stack(grid, a direct childformset todisplay: contents) →__title,__subtitle,__field/__label/__input,__submitand__submit--ghost,__notice. - The short-viewport scroll holds:
.v2-rootis 100vh with overflow hidden and is a flex column,.v2-loginscrolls, and the card centres withmargin: auto. - Phone width: the 480px media rule sets
.v2-login__input { font-size: 16px }at specificity (0,1,0). The code input's rule.v2-cli-authorize .v2-login__input.v2-cli-authorize__codeis (0,3,0) and setsfont: 600 18px/1 var(--v2-font-mono), so mono 18px holds at every width.
4. Accessibility.
- Each state renders exactly one h1 (loading has none, as on main).
- There is a polite live region, and outcomes get
role=status/role=alertthrough V2AuthNotice (status rows mount empty and fill a frame later, so they get announced). - The input is wrapped in a
<label>with visible text and also hasaria-label="Device code". - Continue, Deny, Authorize and Sign in to continue are all
<button>s with explicittype. - The caution is
role="note"with an info glyph, so it doesn't rely on colour.
5. Tests and the rest of the repo.
CI=true npx jest V2CliAuthorize.test.tsx signalAccent.test.ts v2-layout-invariants.test.ts(Node 22): 3 suites and 377 tests pass. The only console output is react-router's future-flag warnings.npx tsc --noEmit -p .(backend lines filtered out): clean.- The component tests assert rendered output: headings by role and name, input value and disabled state, the dt order of the facts, the dd text, the note and status/alert text including the
<code>commonly login</code>, button order and parent, and the real router landing on/v2/login?next=…. They also cover the classes. New coverage: denied, error (alert) and loading. git grepforv2-cli-authorize__card|__primary|__secondary|__field|__mark|__eyebrow|__warning|__actions, "CLI SIGN-IN", "Authorize Commonly CLI" and "Sign in to continue" across frontend, e2e/, docs, cli and backend finds no stale consumers. The only hits are the new artboard and an unrelated i18n string. e2e/ has no/cli/authorizespec. The cli and backend tests reference only the URL, which is unchanged.- The removed signalAccent assertions (warning-ink token,
__primaryoverrides, accent fill) and the reversed "CLI SIGN-IN" assertion are covered by the d215 frames cited in the PR body.
Re-stamp at cdae9b7
Delta reviewed: git diff 47264e66 cdae9b7f, 4 files, +12/-3.
- Note 2 resolved.
index.tsx:23now tests/^\/cli\/authorize\/?$/. Checked by hand: it matches/cli/authorizeand/cli/authorize/, and does not match/cli/authorizexor/cli/authorize/x. - Note 3 resolved. The new test in
V2CliAuthorize.test.tsx:61-69first clearsv2-canvas, then renders the page and checks the body has the class. Afterunmount()it checks the class is gone. The test file doesn't importindex.tsx, so only the component's own effect can satisfy the first check, and only its cleanup can satisfy the second. The test therefore exercises the behaviour it names, which matches the coordinator's report that it fails with the add removed (I didn't re-mutate a read-only worktree). - The
signalAccent.test.ts:128source check now follows the regex. It stays a source-text check; the behaviour is covered by the new render test. - Note 5 resolved. Compared with origin/main, the artboard now has exactly one
.authcard .subrule, and the PR's net artboard diff no longer touches it. - Note 1: the coordinator reports the PR body now states the button-not-link change as a deliberate trade. Accepted. Note 4 is unchanged and was already there before this PR.
- Re-run at this head:
CI=true npx jest V2CliAuthorize.test.tsx signalAccent.test.ts v2-layout-invariants.test.ts: 3 suites, 378 tests pass, one more than before.tsc --noEmit(backend lines filtered out) is clean. - The delta changes nothing in
V2CliAuthorize.tsxor.css, so the device-flow, canvas-lifecycle, CSS and accessibility checks above still hold.
GATE: PASS
/cli/authorizeis wherecommonly loginsends you to approve a device code. It was the last auth screen still drawn as its own card. It now sits on the shared auth card, like sign-in, sign-up, recovery and the invite page, and matches the d215s-cli-authorizeframes (added in this PR).What changes
/v2/login?next=…with the code kept. It's now a button that navigates rather than a link, so it takes the family's key style (the shared rule stylesbutton.v2-login__submitonly), the same trade the invite page made. It no longer opens in a new tab on middle-click.?code=prefill and?next=return.The App.css leak, fixed at its gate. This page mounts outside V2App, so
<body>lackedv2-canvas, and App.css's legacy input, button and form rules (all gated onbody.modern-ui:not(.v2-canvas)) reached the card. The old page out-ranked them with restated overrides. The page now claimsv2-canvaswhile mounted, the same way V2App does, andindex.tsxstamps it at boot for this path (with or without a trailing slash) so there's no dark flash. A render test checks the class on mount and its removal on unmount. No override rules remain.Checks
src/v2: 185 suites pass (V2AgentSettingsPagetimes out only under the full parallel run, and passes alone; untouched here).tscis clean..v2-rootwrapper, short-screen scroll) are kept and now assert the canvas claim instead of the overrides.🤖 Generated with Claude Code
https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3