Files
LocalAI/.agents/coding-style.md
mudler's LocalAI [bot] 6394909557 refactor(ui): move inline styles onto the design system, add an inline-style gate (#11238)
* refactor(ui): add layout/text primitives and per-page CSS blocks

The React UI already shipped a design system (tokens, form grids, data
tables, stat cards, callouts) that the pages largely bypassed: ~2,000
`style={{ ... }}` literals across src/pages and src/components. Each one is
a spacing or colour decision made locally, so no two pages share a rhythm,
which is the main reason the app reads as unfinished rather than as one
product.

Two additions, both to App.css:

  - A small semantic primitive layer: .stack / .hstack for vertical and
    horizontal rhythm, .text-note / .text-sub / .text-meta / .text-mono for
    the text roles the pages kept re-deriving, .tone-* + .icon-chip for
    semantically tinted icons, plus scale-locked spacing and size steps.
    Deliberately short and semantic, not a utility framework: the size and
    spacing classes exist mainly so that an OFF-scale value stays an inline
    style and therefore stays visible.

  - Named blocks for the shapes fifteen pages actually have (.p2p-diagram,
    .usage-tile, .tr-code, .mw-badge, .set-rail, ...), so those shapes are
    defined once instead of per call site.

Two findings worth recording. The type scale is xs 0.6875 / sm 0.8125 /
base 0.875, and 0.75rem was in use roughly 100 times without being on it
(along with 0.7, 0.85, 1.1 and 0.625rem); all now snap to the nearest step.
And there were thirteen distinct table column widths across the app where
three or four would do; they are pulled into .col-w-* so the ladder is
visible in one place, ready to normalise separately.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Bash] [Edit] [Write]

* refactor(ui): move repeated inline styles onto shared classes

Six passes over src/pages and src/components, each matching a whole
`style={{ ... }}` attribute exactly so the swap is provably equivalent:

  - the identical "waiting for the first response" wrapper, repeated
    verbatim in 21 files
  - text roles (size + colour combinations) onto .text-note / .text-meta /
    .text-sub / .text-mono
  - semantic colours, type-scale steps, .panel-title, .list-row
  - spacing and weight steps, .stack / .hstack rows
  - table column widths, small pills, chart legend swatches
  - the remaining shapes appearing three or more times

One class of bug is worth calling out, because it is what a careless
style-to-class conversion produces and it is invisible to every check we
run. Adding `className="x"` to an element that already had a className
leaves TWO className attributes; JSX keeps the last and silently drops the
first, so `<i className={icon} className="text-xs" />` loses its icon while
passing eslint, `vite build` and the full Playwright suite. 112 of these
were introduced and repaired here. The gate added in a later commit fails
on them.

No visual change is intended beyond snapping off-scale font sizes onto the
type scale. Verified after every pass: eslint 0 errors, vite build passes,
Playwright page-render-smoke + navigation 22/22.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Bash] [Edit] [Write]

* refactor(ui): convert fifteen pages onto named classes, add inline-style gate

Full per-page conversions, each one reading the page, naming the shapes it
actually has, and leaving inline only what is computed at runtime:

  P2P 205->0   Traces 50->0   ConfigFieldRenderer 23->0   NodeDetail 29->1
  ModelEditor 25->1   NodeInstallPicker 29->1   FineTune 175->2
  Nodes 33->2   ImportModel 35->3   Talk 43->4   AgentJobDetails 27->4
  Usage 97->6   Settings 24->6   Middleware 54->8   Backends 87->45

Every remainder is genuinely dynamic: a data-driven badge colour, a
`width: ${pct}%`, a tooltip's coordinates.

Naming the shapes made reuse fall out on its own. Nodes reuses the P2P
setup shapes (both present the same "no workers yet, here is how to add
one" flow) and Model Editor reuses the Settings section rail, which was
byte-identical. Two shared *style objects* also turned out to be classes
wearing a costume and were deleted: `monoCell` in Usage and `hintStyle` in
ImportModel.

Some fixes fell out of the conversion. The evaluation toggle on the
fine-tuning page was a hand-rolled div that rendered as a clipped circle;
it is the existing Toggle component now. That page's empty state pointed at
a "New Job" button that was scrolled off the top of the page, and now
carries its own call to action. And `.input--file` is added at the system
level rather than as a local hack, because ImageGen and VideoGen truncate
their file inputs the same way today.

scripts/inline-style-gate.mjs is the ratchet that keeps this from
regressing. It does not forbid inline styles; it fails when the total goes
UP (same discipline as the coverage baseline) and when an element carries
two className attributes. eslint would catch the latter via
react/jsx-props-no-duplicate-props, but that needs eslint-plugin-react,
which this project does not depend on, so the check lives next to the tool
that causes the problem.

  npm run lint:inline-styles          # check against baseline
  npm run lint:inline-styles:report   # per-file counts, worst first
  npm run lint:inline-styles:write    # refresh after converting

Net: 2,061 -> 611 inline styles. eslint 0 errors, vite build passes,
Playwright page-render-smoke + navigation 22/22, gate green on both checks.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [Bash] [Edit] [Write]

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-30 23:17:06 +02:00

5.9 KiB

Coding Style

The project has the following .editorconfig:

root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.go]
indent_style = tab

[Makefile]
indent_style = tab

[*.proto]
indent_size = 2

[*.py]
indent_size = 4

[*.js]
indent_size = 2

[*.yaml]
indent_size = 2

[*.md]
trim_trailing_whitespace = false
  • Use comments sparingly to explain why code does something, not what it does. Comments are there to add context that would be difficult to deduce from reading the code.
  • Prefer modern Go e.g. use any not interface{}

Logging

Use github.com/mudler/xlog for logging which has the same API as slog.

Go tests

All Go tests — including backend tests — must use Ginkgo (v2) with Gomega matchers, not the stdlib testing package with t.Run / t.Errorf. A test file should register a suite with RegisterFailHandler(Fail) in a TestXxx(t *testing.T) bootstrap and use Describe/Context/It blocks for the actual cases. Look at any existing *_test.go under core/ or pkg/ for a template.

Do not mix styles within a package. If you are extending tests in a package that already uses Ginkgo, keep using Ginkgo. If you find stdlib-style Go tests in the tree, treat them as tech debt to be migrated rather than as a pattern to follow.

This is enforced by golangci-lint via the forbidigo linter (see .golangci.yml); calls like t.Errorf / t.Fatalf / t.Run / t.Skip / t.Logf are flagged. Run make lint locally before submitting; the same check runs in CI (.github/workflows/lint.yml).

Outbound HTTP

All outbound HTTP must go through github.com/mudler/LocalAI/pkg/httpclient rather than the standard library's default client. Use httpclient.New(...) (no body deadline — safe for streaming/SSE) or httpclient.NewWithTimeout(d, ...) (simple request/response). Both refuse redirects by default and set a TLS 1.2 floor.

The reason is GHSA-3mj3-57v2-4636: the std default client follows redirects, and on a cross-host redirect Go forwards custom credential headers (e.g. Anthropic's x-api-key) to the redirect target, leaking the secret. httpclient fails closed instead.

  • Need to follow redirects (download CDNs, registry blobs, GitHub asset URLs)? Pass httpclient.WithFollowRedirects() — it still strips credential headers on any cross-host hop.
  • Have a custom transport (IP-pinned dialer, HTTP/2 tuning, a credential-injecting RoundTripper)? Pass httpclient.WithTransport(rt), basing the transport on httpclient.HardenedTransport() to keep the TLS floor. Handed a *http.Client by a library? httpclient.Harden(c) applies the policy in place.

This is enforced by forbidigo (see .golangci.yml): http.DefaultClient and http.Get/Post/PostForm/Head are flagged. The &http.Client{} composite literal can't be matched precisely by forbidigo without also flagging legitimate *http.Client type references, so that form is caught by review — don't construct raw clients.

Documentation

The project documentation is located in docs/content. When adding new features or changing existing functionality, it is crucial to update the documentation to reflect these changes. This helps users understand how to use the new capabilities and ensures the documentation stays relevant.

  • Docs-with-code rule: When you change user-facing behavior (API endpoints, CLI flags, config keys, or features), update the corresponding page under docs/content/ in the SAME change, not as a follow-up. A user-facing change without a matching docs update is incomplete. The PR template carries a checklist item for this.
  • Feature Documentation: If you add a new feature (like a new backend or API endpoint), create a new markdown file in docs/content/features/ explaining what it is, how to configure it, and how to use it.
  • Configuration: If you modify configuration options, update the relevant sections in docs/content/.
  • Examples: providing concrete examples (like YAML configuration blocks) is highly encouraged to help users get started quickly.
  • Shortcodes: Use {{% notice note %}}, {{% notice tip %}}, or {{% notice warning %}} for callout boxes. Do not use {{% alert %}} — that shortcode does not exist in this project's Hugo theme and will break the docs build.

React UI styling

The React UI ships a design system in core/http/react-ui/src/App.css: design tokens, form grids, data tables, stat cards, callouts, plus a small semantic primitive layer (.stack, .hstack, .text-note, .text-meta, .tone-*, .icon-chip). Use it instead of style={{ ... }}. Inline styles are a spacing or colour decision made in one file, so no two pages end up sharing a rhythm, which is the main reason the app reads as unfinished.

Inline styles are still correct for values that are genuinely computed at runtime: width: ${pct}%, a data-driven background, a tooltip's coordinates. Everything else belongs in a class.

A ratchet enforces this:

cd core/http/react-ui
npm run lint:inline-styles          # fails if the count went UP
npm run lint:inline-styles:report   # per-file counts, worst first
npm run lint:inline-styles:write    # refresh the baseline after converting

The gate also fails on duplicate className attributes on one element. JSX keeps the last and silently drops the first, so <i className={icon} className="text-xs" /> loses its icon while passing lint, the build and the e2e suite. Converting a style to a class on an element that already has a className is the usual way to introduce one; merge them into a single attribute instead.

When converting a page, prefer naming the shapes it actually has (.p2p-diagram, .usage-tile) over adding more utilities, and check whether an existing block already covers it: the Nodes page reuses the P2P setup shapes, and Model Editor reuses the Settings section rail.