agentleFS
gpustackgpustack/gpustack-ui/CLAUDE.md

gpustack-ui

CLAUDE.md100 starsChanged 37 days agoApache-2.0View on GitHub

This is the open source UI (gpustack-ui). Common components, hooks, and utils are published as @gpustack/core-ui and consumed throughout src. Always prioritize reusing common components, hooks, and utils from @gpustack/core-ui. Task-specific conventions live in skills: use create-crud-page when building a page module, form-patterns when building cascading/dependent forms. Keep data flow explicit, predictable, and performant. The triggering action is the source of truth for UI updates — not effect-driven synchronization. Do not use request functions as useEffect dependencies. Trigger requests explicitly…

The file

Show its rules and text

We split this CLAUDE.md into the rules it gives an agent

16 rules. About 0 are common, 6 are distinctive, and 10 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 10

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Keep data flow explicit, predictable, and performant.

    line 11
  2. Very specific:

    Do not use request functions as useEffect dependencies.

    line 15
  3. Distinctive:

    If later requests depend on interactions, trigger them inside the interaction handler.

    line 27
  4. Very specific:

    Do not rely on useEffect dependency changes.

    line 28
  5. Very specific:

    When one action updates multiple related states, update them all directly in the handler.

    line 39
  6. Very specific:

    If multiple states always update together, use a single state object instead of multiple useState calls — fewer rerenders, more predictable …

    line 51
  7. Very specific:

    Keep request execution, state updates, and derived calculations close to the triggering action.

    line 59
  8. Very specific:

    Do not use useMemo / useCallback unless there is a confirmed bottleneck.

    line 72
  9. Distinctive:

    Prefer action-driven updates, explicit handlers, and localized state transitions over effect-driven synchronization, cross-hook implicit …

    line 80
  10. Very specific:

    Future direction (apply to all new code): avoid styled-components. Prefer:

    line 84
  11. Very specific:

    Complex table cell: when extracted, name it {feature}-cell.tsx.

    line 117
  12. Distinctive:

    Use these questions to review a proposed boundary.

    line 125
  13. Very specific:

    Keep the existing use-{feature}-columns convention for table column definitions.

    line 137
  14. Distinctive:

    Keep a shared child's whole dependency chain at page level.

    line 172
  15. Very specific:

    Always check @gpustack/core-ui first.

    line 186
  16. Distinctive:

    When building a form, select the add-item component from the shape of the field's data (its schema).

    line 216

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

Repo

This is the open source UI (gpustack-ui). Common components, hooks, and utils are published as @gpustack/core-ui and consumed throughout src.

Always prioritize reusing common components, hooks, and utils from @gpustack/core-ui.

Task-specific conventions live in skills: use create-crud-page when building a page module, form-patterns when building cascading/dependent forms.

React State and Request Patterns

Keep data flow explicit, predictable, and performant. The triggering action is the source of truth for UI updates — not effect-driven synchronization.

1. Avoid effect-driven requests

Do not use request functions as useEffect dependencies. Trigger requests explicitly from user actions or lifecycle entry points.

// Avoid
useEffect(() => {
  fetchData();
}, [fetchData]);

2. Form requests should be action-driven

  • Fetch form data (e.g. Select options) when the form first opens.
  • If later requests depend on interactions, trigger them inside the interaction handler.
  • Do not rely on useEffect dependency changes.
// Recommended
const handleOnChange = (value) => {
  fetchData(value);
};

3. Update related states together

When one action updates multiple related states, update them all directly in the handler. Do not sync via useEffect or derive indirectly via useMemo.

const handleOnChange = (value) => {
  setState1(...);
  setState2(...);
  buildState(...);
};

4. Group strongly related state

If multiple states always update together, use a single state object instead of multiple useState calls — fewer rerenders, more predictable transitions.

const [state, setState] = useState({ state1: ..., state2: ..., state3: ... });

5. Prefer explicit state flow

Keep request execution, state updates, and derived calculations close to the triggering action. Avoid chaining business logic through multiple useEffect hooks.

// Prefer
const handleAction = () => {
  fetchData();
  setTableData(...);
  setSelectedRow(...);
};

6. Avoid premature memoization

Do not use useMemo / useCallback unless there is a confirmed bottleneck. Overuse adds complexity, obscures state flow, and risks stale dependencies. Optimize only when necessary.

7. Keep request logic predictable

A user interaction should clearly show: what request fires, which states update, how the UI changes. Avoid indirect update chains from dependency-driven effects.

8. Prefer action-driven architecture

Prefer action-driven updates, explicit handlers, and localized state transitions over effect-driven synchronization, cross-hook implicit updates, and reactive chains between states.

Styles

Future direction (apply to all new code): avoid styled-components. Prefer:

  1. createStyles for component-scoped dynamic styles
  2. CSS Modules (xxx.module.less) for structured static styles

Existing styled-components usage is legacy tech debt — do not migrate it wholesale, but do not add new styled-components either. Theme tokens (var(--ant-color-*)) work in all three approaches.

Layout

Compose layout with Ant components, not hand-written display: flex.

  • 1D flex (row/column with gap, align, justify) → Flex. Do not write raw display: flex in new code.
  • Inline sequence of a few elements with uniform spacing → Space.
  • Page/grid columns → Row / Col.

Drive spacing with the theme scale (Flex/Space gap, or var(--ant-*) spacing tokens), not scattered px literals.

Tables

Two implementations coexist. Reach for core-ui's Table (conventionally imported as SealTable) in new and migrated pages; antd Table survives only where a page hasn't been migrated yet. They are not interchangeable — core-ui's is a CSS-grid table, not an antd wrapper, so scroll behaves differently on each. Ref src/pages/resources/components/workers.tsx.

  • core-ui Table columns: span is a column's proportional share of the leftover width, as an fr track — leave it off to divide the width evenly, since every column defaults to 1fr. Only set it where a column genuinely needs more or less room than its neighbours, and then keep the spans summing to a round grid. What every column should carry is a minWidth floor: spans share only the width left over once every floor is satisfied, so the floors are what decide when the table starts scrolling.
  • Horizontal scroll, core-ui Table: scroll={{ x: true }} widens the row out to the columns' own floors (sum of minWidth + the prefix gutter) and scrolls past that. An explicit px value works too. Do not pass 'max-content': on a grid of fr tracks the greediest cell sets the fr unit for every track, so one wrap-happy cell (a LabelCell full of tags) inflates the whole table — measured at 4694px against the 1370px the columns actually needed. No extra class: the scrollbar styling ships with the component.
  • Vertical scroll, core-ui Table: scroll={{ y }} caps the body (antd's semantics) and sticks the header to the top of the same scroll viewport. Note that CSS forces overflow-x to auto alongside overflow-y: auto, so a y-only table whose columns are wider than the viewport will still show a horizontal scrollbar.
  • Horizontally scrollable antd Table: set scroll={{ x: 'max-content' }} and add className="scroll-table" on the Table. The class styles the horizontal scroll to match the design; without it the scroll works but looks off. 'max-content' is right here — antd sizes real table columns by content, so it has no fr-unit amplification to worry about.

Naming conventions

A page module lives under src/pages/{module} with this sub-structure: components/, config/, forms/, hooks/, services/, index.tsx. File naming:

  • Create/edit modal: add-{feature}-modal.tsx (keep the -modal suffix even when built with FormDrawer).
  • Table columns hook: use-{feature}-columns.tsx.
  • Open/close & request hooks: use-{verb}-{noun}.ts (e.g. use-create-user.ts, use-query-user-list.ts).
  • Complex table cell: when extracted, name it {feature}-cell.tsx.

Module structure & file size

Split for clear responsibilities and separation of concerns

The purpose of splitting is to make responsibilities explicit and separate concerns. In this section, splitting a component means extracting it into a separate file. Defining a child component within the same file is outside the scope of this file-organization rule.

Use these questions to review a proposed boundary. Line count, naming, and interface size are signals to inspect, not automatic reasons to require or reject a split:

1. What responsibility would each file own? Identify the concerns and the reasons each may change. Extract a component when it gives a concern a clear owner and separates it from the parent's responsibilities. Fetching, filtering, column definitions, and layout may warrant separate owners, but a small list can keep them together coherently. Shared state does not prevent a split: related sections of a form can share state owned by their parent while each owns a distinct concern.

Closely related code often benefits from staying together, such as a simple column and its cell renderer, a form and its field renderers, or a request and its response mapping. A cell or form section with its own substantial interaction, state, or domain responsibility may warrant a separate component. Check that extraction clarifies ownership rather than merely moving JSX or forwarding parameters to another file.

2. Can the extracted responsibility be named clearly? Names such as instance-status-cell, use-keys-columns, and use-query-benchmarks help explain ownership. Difficulty finding a meaningful name is a reason to reconsider the boundary. A domain name alone does not prove a good split, and a utility filename alone does not invalidate one; assess the responsibility the file actually owns.

3. Does the interface express that responsibility clearly? Several props or hook arguments, a whole form instance, or a setState callback are signals to inspect coupling and state ownership. There is no fixed prop limit: value, options, disabled, and onChange can form a coherent interface, while one large object can hide unrelated dependencies. Prefer focused inputs and callbacks that describe the component's actions. Let each consumer obtain shared setup such as useStyles, intl, and context hooks locally when appropriate.

At ~600 lines, review the responsibilities and boundaries using these questions. This is a review threshold, not a file-size limit. A shorter file may need separation, and a longer file may remain cohesive. Consider maintenance, testing, reuse, and cross-file navigation costs when evaluating whether the proposed split achieves clearer responsibilities.

Keep the existing use-{feature}-columns convention for table column definitions. Simple cell renderers can stay with their columns; complex cells can be extracted using the same responsibility checks. Ref src/pages/api-keys/hooks/use-keys-columns.tsx.

Place extracted components near their owner

By default, put an extracted component in the same directory as the original component, using a meaningful business name. Do not create a directory solely to hold one extracted component.

Follow an established directory convention when applicable. Components shared across tabs or modules belong in the appropriate shared directory; a group that develops into a complete submodule may get its own directory. Hooks, pure transforms, constants, and types continue to follow their existing naming and directory conventions. The tab ownership rules below still apply.

One directory per tab

If a page is composed of multiple tabs, each tab gets its own directory under the page directory, with the same internal structure as a page (components/, config/, forms/, hooks/, services/, index.tsx — only the parts it needs). Do not flatten every tab's components into the page-level components/.

The page-level components/, hooks/, config/, and services/ are reserved for things shared across tabs; anything used by a single tab belongs to that tab's directory.

src/pages/usage/
  components/        # shared across tabs only
  config/
  hooks/
  services/
  index.tsx          # tab host
  events-tab/
    services/
    index.tsx
  summary-tab/
    components/
    hooks/
    index.tsx

Name the directory after the tab. Both {tab}-tab/ (e.g. src/pages/usage/events-tab) and plain {tab}/ (e.g. src/pages/gpu-service/instances) exist today — stay consistent with whatever the page already uses.

Two rules of thumb for deciding where a file lives:

  • A component used by two or more tabs goes to the page-level components/ — even when one of them "owns" it conceptually. Putting it in the owner's directory would force the other tab into a ../{sibling-tab}/components/... import, and a cross-tab-directory import is the signal that the file belongs one level up.
  • Keep a shared child's whole dependency chain at page level. Moving the parent up but leaving its children in a tab directory just relocates the same cross-directory import.

The converse also holds: a file sitting in any page-level directory (components/, hooks/, utils/, styles/) with only one consumer tab belongs in that tab's directory.

Config & types

  • config/types.ts — TypeScript types. Form shape → FormData; table/list row → ListItem.
  • config/index.ts — static constants, enums, and value/label maps (e.g. XxxStatusValueMap, XxxStatusLabelMap). Keep constants out of types.ts.
  • Select options that need i18n: set label to the message key and add locale: true on the option — the field translates it at render. Omit locale for options whose label is already final text. Ref src/pages/benchmark/config/index.ts.

When types.ts grows past a few hundred lines and covers several backend domains, promote it to a config/types/ directory: one file per domain (instance.ts, template.ts, storage.ts, …), shared primitives in common.ts, and an index.ts barrel that export type * re-exports all of them. The barrel keeps from '../config/types' resolving unchanged, so no import site has to move. Always import from the barrel, not from a domain file directly, and keep the domain files types only — export type * silently drops a const, so constants belong in config/index.ts. No page uses the directory form yet — src/pages/llmodels/config/types.ts is the first candidate if it keeps growing.

Common components

Always check @gpustack/core-ui first. Frequently reused:

  • Drawer/Modal open/close: useBodyScroll.
  • Form drawer / footer: FormDrawer, ModalFooter.
  • Form modal: no shared wrapper yet — FormDrawer has 34 consumers, its modal sibling was never written, so a page that wants a dialog instead of a drawer hand-rolls Modal + ModalFooter + useSubmitLock. Two do today (src/pages/gpu-service/templates/components/edit-yaml-modal, src/pages/profile/components/modify-password-modal) and they host different content, which is why there is no abstraction yet. At the third, extract FormModal into core-ui and give it FormDrawer's exact props, so switching a page between drawer and dialog is one import.
  • Delete confirmation: DeleteModal.
  • Search + bulk actions bar: FilterBar.
  • Form fields: BaseSelect, Input (labeled).
  • Text overflow: AutoTooltip.
  • Icons: IconFont.
  • Tags & status (4 variants): see the section below.
  • Permission-gated visibility: Access / useAccess.
  • Request hooks: useRequest / useQueryData / useQueryDataList.
  • Table data fetching: useTableFetch.
  • Submit guard (prevent double-submit): useSubmitLock.
  • Tabbed forms: ScrollSpyTabs.

Tags & status indicators

Four core-ui components cover tag/status display in tables and lists. Pick by what the value means, not by how it looks — don't reach for a generic antd Tag:

  • StatusTag — semantic status with a dynamic message/detail (tooltip, download, extra content). Use when a row's status carries variable text, e.g. a failed job with an error message. Colors come from StatusColorMap (error/warning/transitioning/success/inactive).
  • StatusDot — colored dot + short label, no message. Use for a plain status/type cell where the value is a fixed enum (e.g. an event-type or log column). Same StatusColorMap palette; inactive dot is quaternary. If the status needs dynamic text, use StatusTag instead.
  • ThemeTag — a standalone category label (independent content, e.g. a permission scope or a model name). Default neutral; wraps antd Tag.
  • TextAttribute — a small neutral pill that is a subordinate annotation following a primary text (e.g. key-name [custom]), not a standalone tag. Manages its own leading margin. Two variants: filled (default) and outlined. Ref the name column in src/pages/api-keys/hooks/use-keys-columns.tsx.

Rule of thumb: semantic + dynamic text → StatusTag; semantic + fixed enum → StatusDot; independent category → ThemeTag; annotation of nearby text → TextAttribute.

Dynamic add-item form fields

When building a form, select the add-item component from the shape of the field's data (its schema). Match the schema, don't hand-roll a list UI:

  • Plain object (key→value map) → LabelSelector.
  • String array → ListInput. Ref src/pages/llmodels/forms/backend-parameters-list.tsx.
  • Object array → MetadataList with a custom item renderer per entry. Ref src/pages/llmodels/forms/model-lora-list.tsx.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.