refactor(desktop): migrate api-key settings onto the design system

Merged
CH
7d ago
+219-104

D16 of the design-system migration (Desktop stack, base feature/desktop-d15-account-settings, #3387).

What

  • Settings › Coding agents › API keys renders through @luke/ui's SettingsSection and SettingsRow, replacing the desktop SettingsGroup and the hand-built .settings-copy markup. Each provider's row leads with its mark through the row's leading slot.
  • MenuRow gains a row form (desktop adapter, not a shared API): given { label, description, leading, control } it draws the shared SettingsRow, with the control and the ⋯ at the row's end. The search anchor and right-click stay on the wrapper, as before. Its other callers (Workspace, Integrations, voice keys) pass children and keep their current markup until their own D rows move them.
  • Add key and Cancel are the barrel Button (outline), Save the barrel Button (primary, type="submit", busy with "Checking the key"). They replace toolbar-button bordered-button and primary-button.
  • The key field is the barrel TextField (PASSWORD). It's now controlled: the pasted key sits in the row's state while the field is open and is cleared once the service keeps it, or when the field is cancelled. It was previously read from the DOM. The key is still sent once, never shown, and gone from the page once kept (existing test).
  • New shared prop (approved by the orchestrator): TextField's autoComplete?: TextFieldAutoComplete with TEXT_FIELD_AUTO_COMPLETE = { OFF: "off" }, so the credential field keeps autocomplete="off" on the input itself. It has a catalog line (the text fields example's API key field) and a jsdom test.
  • Escape in the field still cancels the editor without leaving Settings, and a composition's Escape is still the IME's. The handler now sits on the form and answers only an Escape from the field.
  • The whole group stays inside ph-no-capture. The field no longer carries the class itself (barrel controls take none) but inherits it from the wrapper, which PostHog applies to the wrapper's children too.
  • Save, replace (Edit key), remove (confirm dialog), the one-change-at-a-time lock, the refusal under the field (role="alert") and the read failure/Retry are unchanged.

Selectors

  • Removed: .api-key-form .settings-input and the .api-key-form branch of .settings-card > … + .error-message. These rules are now container rules on the new .api-key wrapper (.api-key-form, .api-key > .error-message) on the contract's role tokens. The narrow-container rule keeps the field on its own line.
  • .settings-copy .settings-state[data-state="attention"] is now .settings-state[data-state="attention"], so the attention tint reaches a shared row's description. Every other settings-state sits inside .settings-copy, so nothing else changes.

Visible changes

  • The provider name uses the label role (13/18, 500) instead of strong. The status line uses the caption role in the subtle colour (12/16), like other migrated rows' descriptions.
  • Buttons and the field take the barrel looks at the same 30px height and 8px radius.
  • Save's busy ring shows as soon as the save goes out, as the barrel Button's busy state does. The old version showed it after 180ms.
  • Below a 380px row, the ⋯ (or Add key) drops below the words to the row's end, as the shared row contract requires. The old row kept it beside the words.

Tests

  • text-field.test.ts: new jsdom test. A field asked to is left out of autofill (autocomplete is off), and one not asked to carries no attribute.
  • api-keys-section.test.ts / coding-agents-page.test.ts: the pasted key is typed through the input's own value setter, as typing does, because the field is now controlled. The row is found by its name's text instead of the retired strong. The recording check now asserts the field is inside the unrecorded wrapper. Every other assertion is unchanged, including autocomplete === "off".
  • pnpm check:affected exits 0.

Evidence

Captured on Linux with headless Chrome at 1280×840. A throwaway, uncommitted harness mounted the real ApiKeysSection in SettingsPage with the built desktop stylesheet, over a fake bridge with synthetic keys (sk-ant-…wxyz, sk-proj-…k3q9).

LightDark
Kept keys: verified, and out of quota
Adding a key, save refused

Page narrowed to 380px (rows under 380px): the field takes its own line, with Cancel and Save below it.

Not verified on a Mac

  • SF Pro rendering of the rows, the marks and the hint's code face
  • The real page against a live account: save, replace and remove were exercised only through the tests and the harness
  • Whether the system's password autofill stays away from the field

🤖 Generated with Claude Code


Summary by cubic

Moves Settings › Coding agents › API keys onto @luke/ui's design system: providers render as shared SettingsRows led by their marks, and Add key, Cancel, Save, and the key field use the barrel Button and TextField instead of the desktop's own controls. Save, replace, remove, the refusal under the field, and Escape-in-field cancellation still behave as before.

MenuRow gains a row form that draws the shared SettingsRow with the control and ⋯ at the row's end; its other callers keep their existing markup. The key field is now controlled — the pasted key lives in row state and clears once saved or cancelled — and stays out of autofill via a new TextField prop (TEXT_FIELD_AUTO_COMPLETE), which has a catalog line and a jsdom test.

Visible changes

  • The provider name and status line use the shared label and caption roles instead of strong.
  • Buttons and the field take the barrel looks at 30px height and 8px radius; Save's busy ring appears immediately rather than after 180ms.