Architecture
@sinups/ai-kit is a UI construction kit for agent products built on Mantine. It covers the chat itself and everything around it: multi-step wizards, settings screens, MCP servers, agents, skills, permissions, hooks, sessions, background tasks and diff review. Every piece works in a narrow widget (about 360px) and in a full-page app (900px and wider).
styles/ --ae-* tokens, keyframes
utils/, hooks/ pure functions and generic hooks, no JSX
primitives/ domain-neutral building blocks (Wizard, SettingsLayout, EntityList, ...)
<chat modules> AgentChat, MessageList, input/, tools/, question/, elicitation/, ...
<domain modules> mcp/, agents/, skills/, permissions/, hooks-config/, sessions/, tasks/,
message-actions/, diff/, model-settings/Dependencies point down only. A domain module may use primitives, utilities and chat modules; a primitive never imports a domain module; domain modules do not import each other except through exported types. Everything public is exported from the package root.
- Presentational and controlled. Components receive data through props and report intent through callbacks (
onSelect,onSave,onReconnect). No fetching, no global stores, no routing: your app owns data and side effects. - Async actions return promises. While the promise is pending the button shows a loader; a rejection message is shown in an alert.
- Every data view handles four states: loading (skeleton), error (alert with optional retry), empty (Mantine
EmptyState) and data. - Logic lives outside JSX. Parsing, validation, filtering and formatting are pure, tested functions exported next to the components; reusable stateful logic is a hook (
useWizard,useFuzzySearch). - Domain types are exported and stay compatible with the source protocol: the MCP specification for MCP components, the AI SDK
UIMessagefor chat. - Labels are English defaults that can be overridden through props, usually
labels. - Width.Layout adapts to the component's own width, not the viewport: side by side when wide (list and detail, navigation and content), stacked with a back action when narrow.
<McpServerList
servers={servers} // data comes from your app
loading={isLoading}
error={loadError}
onRetry={reload}
onReconnect={(server) => api.reconnect(server.id)} // a promise: the action shows pending state
onRemove={(server) => api.remove(server.id)} // a rejection is shown in an alert
labels={{ addServer: "Connect server" }} // English defaults, override any label
/>Components are built from Mantine components and the Styles API, so they follow your theme, color scheme and fonts. Peer dependencies are only @mantine/core, @mantine/hooks and @tabler/icons-react. Styles use --mantine-* and --ae-* variables, which you can override on any ancestor.
Domain modules such as MCP, Permissions and Hooks are assembled from these domain-neutral building blocks. Use them directly for your own screens.
<module>/<Name>/<Name>.tsx memo component, JSDoc on every prop
<module>/<Name>/<Name>.module.css optional
<module>/<Name>/<Name>.story.tsx Usage + Narrow + Wide (+ states: Loading, Error, Empty)
<module>/<Name>/<Name>.test.tsx behavior tests with @mantine-tests/core + user-event
<module>/<logic>.ts + .test.ts pure logicSmall modules keep components flat in the module folder, for example tools/ and input/. Every component has stories for a narrow and a wide container.
In the repository, yarn test runs dependency checks, formatting, type checking, lint and unit tests; yarn build builds the package and yarn storybook serves the stories.