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.