mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore(site): enforce sentence-case UI labels in AgentsPage via Biome plugin
Add a Biome GritQL plugin scoped to src/pages/AgentsPage non-test files that flags Title Case in label-bearing JSX attributes (label/title/sectionLabel/aria-label) and label:/title: object properties. Fix the residual violations it surfaces (API key, Custom headers, User OIDC identity, Force on, Default on, Default off) and document the convention in site/AGENTS.md.
This commit is contained in:
@@ -117,6 +117,15 @@ Debug logs and pprof dumps use the same job name and commit SHA convention.
|
|||||||
- For JSX boolean props that are `true`, use the shorthand form
|
- For JSX boolean props that are `true`, use the shorthand form
|
||||||
(`<Foo prop />`) instead of `<Foo prop={true} />`. The two are
|
(`<Foo prop />`) instead of `<Foo prop={true} />`. The two are
|
||||||
equivalent; the shorthand is the React convention and reduces noise.
|
equivalent; the shorthand is the React convention and reduces noise.
|
||||||
|
- Use **sentence case** for user-facing UI labels: capitalize only the
|
||||||
|
first word and proper nouns (`Personal instructions`, `API key`), not
|
||||||
|
Title Case (`Personal Instructions`, `API Key`). Under
|
||||||
|
`src/pages/AgentsPage`, a Biome GritQL plugin
|
||||||
|
(`biome-rules/use-sentence-case-labels.grit`) enforces this for
|
||||||
|
`label`/`title`/`sectionLabel`/`aria-label` attributes and `label:`/
|
||||||
|
`title:` object properties. It cannot see labels rendered as JSX text
|
||||||
|
or built from variables, so those still need a human eye. Add genuine
|
||||||
|
proper nouns to the allowlist in that file.
|
||||||
- **Avoid unnecessary indirection.** Inline single-use module-level
|
- **Avoid unnecessary indirection.** Inline single-use module-level
|
||||||
constants, single-use aliases, and one-line helpers that just return a
|
constants, single-use aliases, and one-line helpers that just return a
|
||||||
single field at the call site. Do not create wrapper hooks that only
|
single field at the call site. Do not create wrapper hooks that only
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Enforce sentence case for user-facing UI labels.
|
||||||
|
//
|
||||||
|
// Flags Title Case in label-bearing JSX attributes (label, title,
|
||||||
|
// sectionLabel, aria-label) and object-property labels (label:, title:,
|
||||||
|
// sectionLabel:). Sentence case capitalizes only the first word and
|
||||||
|
// proper nouns: "Personal instructions", not "Personal Instructions".
|
||||||
|
//
|
||||||
|
// Heuristic: a space followed by a capitalized word ([A-Z][a-z]). This
|
||||||
|
// also catches acronym-prefixed Title Case like "MCP Servers". Genuine
|
||||||
|
// multi-word proper nouns (e.g. "VS Code") are false positives and must
|
||||||
|
// be added to the allowlist below.
|
||||||
|
//
|
||||||
|
// Diagnostic-only: Biome GritQL plugins cannot autofix yet.
|
||||||
|
language js
|
||||||
|
|
||||||
|
or {
|
||||||
|
JsxAttribute(name = $name) as $node where {
|
||||||
|
$name <: r"^(?:label|title|sectionLabel|aria-label)$",
|
||||||
|
$node <: contains JsxString() as $value
|
||||||
|
},
|
||||||
|
JsPropertyObjectMember() as $node where {
|
||||||
|
$node <: contains JsLiteralMemberName() as $key,
|
||||||
|
$key <: r"^(?:label|title|sectionLabel)$",
|
||||||
|
$node <: contains JsStringLiteralExpression() as $value
|
||||||
|
}
|
||||||
|
} where {
|
||||||
|
$value <: r".* [A-Z][a-z].*",
|
||||||
|
// Allowlist: proper nouns / product names that are correctly multi-cap.
|
||||||
|
not $value <: r".*(?:VS Code|Coder Agents|GitHub Actions|JetBrains Fleet|Cmd/Ctrl).*",
|
||||||
|
register_diagnostic(
|
||||||
|
span = $value,
|
||||||
|
message = "Use sentence case for UI labels (e.g. \"Personal instructions\"), not Title Case. If this is a proper noun, add it to the allowlist in use-sentence-case-labels.grit.",
|
||||||
|
severity = "warn"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,5 +3,16 @@
|
|||||||
"files": {
|
"files": {
|
||||||
"includes": ["!e2e/**/*Generated.ts", "!scripts/*.mjs"]
|
"includes": ["!e2e/**/*Generated.ts", "!scripts/*.mjs"]
|
||||||
},
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"includes": [
|
||||||
|
"src/pages/AgentsPage/**/*.ts",
|
||||||
|
"src/pages/AgentsPage/**/*.tsx",
|
||||||
|
"!**/*.stories.tsx",
|
||||||
|
"!**/*.test.tsx"
|
||||||
|
],
|
||||||
|
"plugins": ["./biome-rules/use-sentence-case-labels.grit"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json"
|
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<ProviderField
|
<ProviderField
|
||||||
label="API Key"
|
label="API key"
|
||||||
htmlFor={apiKeyInputId}
|
htmlFor={apiKeyInputId}
|
||||||
required={requiresAPIKey}
|
required={requiresAPIKey}
|
||||||
description={apiKeyDescription}
|
description={apiKeyDescription}
|
||||||
|
|||||||
@@ -67,25 +67,25 @@ const TRANSPORT_OPTIONS = [
|
|||||||
const AUTH_TYPE_OPTIONS = [
|
const AUTH_TYPE_OPTIONS = [
|
||||||
{ value: "none", label: "None" },
|
{ value: "none", label: "None" },
|
||||||
{ value: "oauth2", label: "OAuth2" },
|
{ value: "oauth2", label: "OAuth2" },
|
||||||
{ value: "api_key", label: "API Key" },
|
{ value: "api_key", label: "API key" },
|
||||||
{ value: "custom_headers", label: "Custom Headers" },
|
{ value: "custom_headers", label: "Custom headers" },
|
||||||
{ value: "user_oidc", label: "User OIDC Identity" },
|
{ value: "user_oidc", label: "User OIDC identity" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const AVAILABILITY_OPTIONS = [
|
const AVAILABILITY_OPTIONS = [
|
||||||
{
|
{
|
||||||
value: "force_on",
|
value: "force_on",
|
||||||
label: "Force On",
|
label: "Force on",
|
||||||
description: "Always injected into every conversation.",
|
description: "Always injected into every conversation.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "default_on",
|
value: "default_on",
|
||||||
label: "Default On",
|
label: "Default on",
|
||||||
description: "Pre-selected but users can opt out.",
|
description: "Pre-selected but users can opt out.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "default_off",
|
value: "default_off",
|
||||||
label: "Default Off",
|
label: "Default off",
|
||||||
description: "Available but users must opt in.",
|
description: "Available but users must opt in.",
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
@@ -784,7 +784,7 @@ const ServerForm: FC<ServerFormProps> = ({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="API Key" htmlFor={`${formId}-apikey-value`}>
|
<Field label="API key" htmlFor={`${formId}-apikey-value`}>
|
||||||
<Input
|
<Input
|
||||||
id={`${formId}-apikey-value`}
|
id={`${formId}-apikey-value`}
|
||||||
className="h-9 font-mono text-[13px] [-webkit-text-security:disc]"
|
className="h-9 font-mono text-[13px] [-webkit-text-security:disc]"
|
||||||
|
|||||||
Reference in New Issue
Block a user