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…
In this repository4 other files it gives its agents.
- AGENTS.mdAGENTS.md
- create-crud-pageSkill
- design-reviewSkill
- form-patternsSkill
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
- Distinctive: line 11
Keep data flow explicit, predictable, and performant.
- Very specific: line 15
Do not use request functions as useEffect dependencies.
- Distinctive: line 27
If later requests depend on interactions, trigger them inside the interaction handler.
- Very specific: line 28
Do not rely on useEffect dependency changes.
- Very specific: line 39
When one action updates multiple related states, update them all directly in the handler.
- Very specific: line 51
If multiple states always update together, use a single state object instead of multiple useState calls — fewer rerenders, more predictable …
- Very specific: line 59
Keep request execution, state updates, and derived calculations close to the triggering action.
- Very specific: line 72
Do not use useMemo / useCallback unless there is a confirmed bottleneck.
- Distinctive: line 80
Prefer action-driven updates, explicit handlers, and localized state transitions over effect-driven synchronization, cross-hook implicit …
- Very specific: line 84
Future direction (apply to all new code): avoid styled-components. Prefer:
- Very specific: line 117
Complex table cell: when extracted, name it {feature}-cell.tsx.
- Distinctive: line 125
Use these questions to review a proposed boundary.
- Very specific: line 137
Keep the existing use-{feature}-columns convention for table column definitions.
- Distinctive: line 172
Keep a shared child's whole dependency chain at page level.
- Very specific: line 186
Always check @gpustack/core-ui first.
- Distinctive: line 216
When building a form, select the add-item component from the shape of the field's data (its schema).
≈ “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.
Files
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.
Selectoptions) when the form first opens. - If later requests depend on interactions, trigger them inside the interaction handler.
- Do not rely on
useEffectdependency 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:
createStylesfor component-scoped dynamic styles- 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 rawdisplay: flexin 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
Tablecolumns:spanis a column's proportional share of the leftover width, as anfrtrack — leave it off to divide the width evenly, since every column defaults to1fr. 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 aminWidthfloor: 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 ofminWidth+ the prefix gutter) and scrolls past that. An explicit px value works too. Do not pass'max-content': on a grid offrtracks the greediest cell sets thefrunit for every track, so one wrap-happy cell (aLabelCellfull 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 forcesoverflow-xtoautoalongsideoverflow-y: auto, so ay-only table whose columns are wider than the viewport will still show a horizontal scrollbar. - Horizontally scrollable antd
Table: setscroll={{ x: 'max-content' }}and addclassName="scroll-table"on theTable. 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 nofr-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-modalsuffix even when built withFormDrawer). - 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.tsxName 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 oftypes.ts.Selectoptions that need i18n: setlabelto the message key and addlocale: trueon the option — the field translates it at render. Omitlocalefor options whose label is already final text. Refsrc/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 —
FormDrawerhas 34 consumers, its modal sibling was never written, so a page that wants a dialog instead of a drawer hand-rollsModal+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, extractFormModalinto core-ui and give itFormDrawer'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 fromStatusColorMap(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). SameStatusColorMappalette;inactivedot is quaternary. If the status needs dynamic text, useStatusTaginstead.ThemeTag— a standalone category label (independent content, e.g. a permission scope or a model name). Default neutral; wraps antdTag.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) andoutlined. Ref the name column insrc/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. Refsrc/pages/llmodels/forms/backend-parameters-list.tsx. - Object array →
MetadataListwith a custom item renderer per entry. Refsrc/pages/llmodels/forms/model-lora-list.tsx.
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
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.

