Controls come from components/ui.
AGENTS.md says how to use and extend them.
Don't style a raw <button>, <input>, <select>, or <textarea> to look like a control a
primitive provides. Raw elements are right when the element isn't that control: semantic rows, tabs,
resize handles, swatches, editor surfaces, anything whose behavior or geometry differs by design.
Style with Tailwind classes in the component that renders the element.
- Global CSS, in
index.cssor any other stylesheet, is only for what classes on the owning component can't express: generated Markdown or imperative DOM, custom elements and shadow roots, animations, runtime theme variables, and browser or Electron integration. - No
style={{ ... }}or inline<style>for static values a class covers. Values computed at runtime are fine. - Theme-only rules use
@variant darkand@variant light, not.darkselectors.
The composer banners are the reference.
ComposerBanner.tsxexports one object of small slots. Slots own their classes, takeclassName, and spread the rest of their props, merging classes withcn. Variants and density are props onRoot. A slot that needs a control rendersButton.ComposerBannerStack.tsxcomposes the slots and owns only stack behavior: priority, expand and collapse, dismiss transitions.- Consumers are a few lines of slots, and the slots carry the styling:
ComposerActivityStatus.tsx.
Build new UI the same way. Compose or extend a component instead of copying its markup and classes.
When a call site repeats a treatment another one already uses, make it a variant or a slot on the
shared component. A new composer banner or notice uses the ComposerBanner slots.
V8 gives every closure created in one render a single shared scope. A memoized handler keeps the scope of the render that created it, and that scope holds the other memoized values of that render. When handlers in one component change at different times, each keeps an older scope that holds older handlers, and old renders, with their server config and thread data, stay in memory for as long as the component is mounted. React Compiler's memoized closures do the same.
In large, long-lived components such as ChatView and ChatComposer, write handlers as plain
functions and wrap them with useStableCallback
only where they leave the component: props, hook dependency arrays, and effect or memo bodies.
Calls inside the component keep the plain function, so work that resumes after an await still
acts on the render it started in. A function that a child calls during render, or after an
await, comes from a module-level factory memoized on its inputs, so its closure keeps only
those inputs.
The compiler memoizes a component or hook only when it can prove the code follows React's rules.
Anything it can't prove, such as reading a ref during render or calling setState from an effect,
makes it skip the whole function without a build error. Lint reports each skip as a
react(...) warning with the note "React Compiler skipped optimizing this component or hook".
Every file under apps/web/src must compile. Lint makes those rules errors there, and
t3code/no-manual-memo rejects useMemo and useCallback, which the compiler makes redundant;
no upstream rule bans them outright. It also rejects "use memo". The compiler withholds a cache
scope when a value reaches plain function calls on both sides of a hook call. A "use memo"
helper forces one, but hides the ordering problem. Fix the order instead: plain uses of a value go
before the hook, or the helper is inlined. A use* helper that calls no hook is compiled as a
plain function and caches nothing, so a derived-value helper reads its source hook itself instead
of taking the value as an argument. t3code/no-hookless-use-function rejects a use* function
that calls no hook. Lint misses some bailouts, notably try with finally and
throw inside try, so reactCompilerCoverage.test.ts
compiles every file and fails on any skip. Async handlers rarely need try: an
atom command settles into a result, so cleanup can follow the await. useStableCallback still
marks handlers that leave a large component, as described above.