@agno-hq/agent-os
v1.0.0
Published
Composable, embeddable React components for AgentOS — a headless chat hook, a provider for building your own layout, and drop-in chat embeds.
Readme
@agno-hq/agent-os
Composable, embeddable React components for AgentOS — a headless hook, a provider for building your own layout, and drop-in embeds for streaming any Agno agent, team, or workflow.
Formerly
@agno-hq/chat-react. Upgrading from it means renaming your imports and nothing else. See Migrating.
| Subpath | Contents |
| ------------------------------ | ---------------------------------------------------------------------- |
| @agno-hq/agent-os | Everything below. |
| @agno-hq/agent-os/core | AgnoClient, run streaming, event helpers, types. No React rendering. |
| @agno-hq/agent-os/chat | useAgnoChat, ChatProvider, AgnoChat, and every building block. |
| @agno-hq/agent-os/styles.css | Tokens + component styles. |
| @agno-hq/agent-os/tokens.css | CSS variables only. |
| @agno-hq/agent-os/fonts.css | Inter + DM Mono (optional). |
It speaks the AgentOS HTTP run protocol directly: it POSTs to the run endpoint, parses the streamed events, and accumulates them into render-ready messages — content, tool calls, reasoning, citations, media — while exposing the raw event feed, live status, and human-in-the-loop pauses.
import { AgnoChat } from '@agno-hq/agent-os/chat'
import '@agno-hq/agent-os/styles.css'
import '@agno-hq/agent-os/fonts.css' // optional — Inter + DM Mono
export default function Page() {
return <AgnoChat baseUrl="http://localhost:7777" allowFiles />
}That's the zero-wiring embed: it discovers the agents/teams/workflows on your
AgentOS and runs a full chat. It renders no header and fills its container,
so it drops into a page, a card, a drawer or a modal without fighting your
layout — put your own chrome in the header slot.
Support chat on every page is the same component in launcher mode:
<AgnoChat mode="launcher" baseUrl="..." launcherLabel="Chat with us" />Compose it instead
<ChatProvider> runs the chat and shares it through context, so every component
below reads it with no props:
import { ChatProvider, MessageList, ChatInput } from '@agno-hq/agent-os/chat'
;<ChatProvider baseUrl="http://localhost:7777" entity={agent}>
<MyOwnHeader />
<MessageList />
<ChatInput />
</ChatProvider>Reach for useChatContext() to drive it from your own components, or call
useAgnoChat directly and render nothing of ours.
Customize it
Three independent levers:
/* 1. Tokens — palette, radii and shadows from agno-os/globals.css */
:root { --color-brand-brand: 99, 102, 241; --agno-radius-lg: 1.25rem; }
/* 2. Slots — every element is addressable by name */
<AgnoChat classNames={{ root: 'rounded-2xl', textarea: 'font-mono' }} />
/* 3. Replacement — swap any piece for your own */
<ChatWindow composer={<MyComposer />} header={<MyBar />} />The built-in classes stay on the elements, so Tailwind utilities layer on top
rather than replacing them. Import only tokens.css (or nothing) to style from
scratch.
Storybook
npm run storybook runs the component gallery locally; every story streams
against a scripted in-memory backend, so it exercises the real hook and the real
event reducers without an AgentOS running. Pushes to main publish it to GitHub
Pages via .github/workflows/storybook.yml.
Install
npm install @agno-hq/agent-osreact and react-dom (>=18) are peer dependencies. The library ships
compiled ESM + CommonJS bundles with bundled type declarations, so it works out
of the box in any modern bundler (Vite, Next.js, Webpack, etc.) with no extra
config. Import the stylesheet once (see below).
The hook: useAgnoChat
import { useAgnoChat, ChatWindow } from '@agno-hq/agent-os'
import '@agno-hq/agent-os/styles.css'
function Chat() {
const chat = useAgnoChat({
baseUrl: 'http://localhost:7777',
entity: { type: 'agent', id: 'agno_assist', name: 'Agno Assist' },
userId: 'user-123'
})
return <ChatWindow chat={chat} />
}entity.type is 'agent' | 'team' | 'workflow' and entity.id is the
agent/team/workflow id — the same hook drives all three.
Buffered streaming
Plain text is batched so the answer reveals in smooth steps instead of one character at a time. The first text of an answer is shown immediately; after that, text accumulates for 150 ms and each flush fades in as one batch. Tool calls, media, pauses, errors and completion are never delayed: each one flushes the pending text ahead of it, so the transcript always stays in order.
<AgnoChat
baseUrl="http://localhost:7777"
streaming={{
flushIntervalMs: 150,
maxBufferEvents: 100,
maxBufferChars: 16_384
}}
/>The same streaming prop works on useAgnoChat and <ChatProvider>.
| Option | Default | Effect |
| ----------------- | ------- | ----------------------------------------------------------------- |
| enabled | true | false delivers every event as it arrives. |
| flushIntervalMs | 150 | How long text accumulates before a flush. 0 disables buffering. |
| maxBufferEvents | 100 | Flush early once this many events are pending. |
| maxBufferChars | 16384 | Flush early once this many characters are pending. |
| animation | 400ms | How each batch fades in: { durationMs }, or false for none. |
Settings are read when a run or continuation starts, so changing the prop
affects the next one. A hidden tab flushes immediately, since nothing is
painting. Invalid values fall back to the defaults. DEFAULT_STREAMING_OPTIONS
and StreamingOptions are exported from the root and /chat entry points.
What stays immediate:
onEventreceives every raw event as it arrives, unbuffered.events,currentEventand each message's event log update per batch, with every original event preserved in order.- Cancelling keeps the text already received. Switching sessions keeps the buffer with its run; deleting the session or unmounting discards it.
AgnoClientandstreamRunare never buffered.
Each batch fades in through Streamdown's own animation, with every word of a
batch starting together. Fenced code stays plain until the run completes, then
highlights. Reduced motion turns the fade off. Pass animated in options on
<Markdown> to change or disable it.
Try Streaming / Buffered in Storybook to compare the defaults, a longer buffer and per-event updates.
What the hook returns
| Value | Type | Description |
| -------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------- |
| messages | ChatMessage[] | Full transcript, oldest first. |
| streamingMessage | ChatMessage \| null | The agent message currently streaming. |
| events | RunEventData[] | Every raw event from the latest run, in order. |
| currentEvent | RunEventData \| null | The most recent event. |
| status | 'idle' \| 'streaming' \| 'paused' \| 'completed' \| 'error' \| 'cancelled' | Run lifecycle. |
| activity | string \| null | Live label, e.g. "Calling get_weather", "Reasoning". |
| isStreaming / isPaused | boolean | Convenience flags. |
| error | string \| null | Last error message. |
| sessionId | string \| undefined | Auto-captured from the first run. |
| tools | ToolExecution[] | Tool calls of the active message. |
| reasoning | ReasoningStep[] | Reasoning steps of the active message. |
| pendingRequirements | RunRequirement[] | Outstanding human-in-the-loop asks. |
Actions
| Action | Description |
| -------------------------------------------- | --------------------------------------------------------- |
| sendMessage(text, { files? }) | Send a message and stream the response. |
| cancel() | Abort the active run (also calls the cancel endpoint). |
| continueRun({ tools?, stepRequirements? }) | Resume a paused run with resolved requirements. |
| respondToConfirmation(approve) | Approve/reject pending tool confirmations, then continue. |
| submitUserInput(values) | Provide values for pending input fields, then continue. |
| reset() | Clear the transcript and start a new session. |
| setMessages(...) | Replace the transcript (e.g. after restoring a session). |
| client | The underlying AgnoClient for discovery/session calls. |
Components
All components are styled by @agno-hq/agent-os/styles.css, which carries the
Agno OS design system — the same colour tokens, radii, Inter/DM Mono type scale
and chat layout as the AgentOS chat page. It is dark by default; add the
agno-light class on a wrapper for light mode (agno-dark is accepted too, for
apps that toggle both). Every piece is exported so you can compose your own
layout.
Theme it by overriding the tokens on your wrapper — they are the AgentOS ones,
as R, G, B triples:
.agno-root.my-brand {
--color-brand-brand: 255, 64, 23;
--color-background: 17, 17, 19;
--color-background-secondary: 39, 39, 42;
}| Component | Purpose |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| <AgnoChat> | All-in-one widget: discovery, selector, chat, optional per-run event feed. |
| <ChatWindow chat={chat}> | Full chat surface built from a useAgnoChat result. |
| <ChatLauncher> | Floating support-widget shell: a corner bubble that opens any chat surface. |
| <QuickPrompts> | Suggested-prompt chips. |
| <MessageList> | Transcript with live status + footer slot. Scrolls to the end when a session opens or a message is sent, never mid-answer. |
| <Message> | A single message: content, behind-the-scenes panel, media, citations, follow-ups, action row. |
| <MessageActions> | The icon row under a finished answer — copy, plus whatever the product adds. |
| <CopyButton> | The copy control, with the "copied" flash and clipboard fallbacks — the one code blocks and answers use. |
| <BehindTheScenes> | One collapsible timeline over a run's reasoning, tools, member turns, steps and events — doubles as the live status line. |
| <ChatInput> | Multiline input with file attach, send, and stop; opens up for @mentions, context chips and a custom textarea. |
| <ToolCalls> | Collapsible tool-call cards (name, args, result, status). |
| <Reasoning> | Collapsible reasoning-steps panel. |
| <Citations> | The sources behind an answer, as linked cards. |
| <Followups> | "Related questions" — the follow-up prompts an agent suggests after its answer. |
| <Multimedia> | Images, video, and audio attachments. |
| <StatusIndicator> | Animated "what is it doing now" line. |
| <EventLog> | Developer feed of raw run events — render it yourself from chat.events. |
| <HumanInput> | Human-in-the-loop panel (confirm / reject / input). |
| <EntitySelector> | Dropdown of agents, teams, and workflows. |
| <SessionList> | Sidebar of past sessions — click to load; each row's menu renames or deletes it. |
| <Markdown> | Markdown, rendered by Streamdown — GFM, and repair for half-written text mid-stream. |
Inline, or a floating support widget
<AgnoChat> renders inline by default — a panel wherever you put it, sized by
width and height:
<AgnoChat baseUrl="http://localhost:7777" width={720} height={560} />mode="launcher" turns it into the support-chat layout instead: a bubble pinned
to a corner of the viewport that opens the chat. Mount it once, anywhere in your
app — it renders into document.body, so no wrapper can clip it.
<AgnoChat
baseUrl="http://localhost:7777"
mode="launcher"
position="bottom-right"
launcherLabel="Chat with us"
width={400}
height={620}
/>| Prop | Default | Description |
| ----------------------- | ---------------- | -------------------------------------------------------------------- |
| mode | 'inline' | 'inline' renders in place; 'launcher' pins a bubble to a corner. |
| position | 'bottom-right' | Corner to pin to: also bottom-left, top-right, top-left. |
| offset | 24 | Distance from both edges of that corner. Number means px. |
| panel | 'popover' | How far it opens: a corner popover, or 'fullscreen' over the page. |
| width / height | 400 / 620 | Size of the popover (and of the inline panel). |
| launcherLabel | — | Text beside the bubble glyph. Icon-only without it. |
| launcherIcon | speech bubble | Replaces the default glyph. |
| defaultOpen | false | Open on first render. |
| open / onOpenChange | — | Control the open state yourself (e.g. from your own "Help" button). |
A 'fullscreen' panel covers the page and closes from an X in the chat header;
a popover closes from the bubble, which becomes an X. Escape closes either, and
below 480px wide a popover goes full-screen on its own. The conversation is kept
across close/open — a run that is still streaming keeps streaming.
To give your own layout the same behaviour, wrap it in <ChatLauncher>:
<ChatLauncher position="bottom-left" panel="fullscreen" label="Support">
<ChatWindow chat={chat} />
</ChatLauncher>Errors
A failed run leaves its message on chat.error, but nothing is rendered by
default — raw backend errors are long, and they crowd a narrow window. Opt in
with showErrors on <AgnoChat> or <ChatWindow>:
<AgnoChat baseUrl="http://localhost:7777" showErrors />Or render it wherever suits your layout — a toast, say:
{
chat.error && <Toast>{chat.error}</Toast>
}Quick prompts
quickPrompts offers suggested prompts while the transcript is empty. Clicking
one sends it. A chip is either the prompt itself, or a short label that sends
something longer:
<AgnoChat
baseUrl="http://localhost:7777"
quickPrompts={[
'What can you do?',
{ label: 'Pricing', prompt: 'How does pricing work?' },
{ label: 'Talk to a human', prompt: 'I would like to talk to a human.' }
]}
/><ChatWindow> takes the same prop, plus onQuickPrompt to do something other
than send the text. Render <QuickPrompts> yourself to place the chips
elsewhere.
Building on the composer
<ChatInput> is the AgentOS dock: an auto-growing textarea, attach, send and
stop. compact trades the dock for one row — the field with the buttons beside
it — for a launcher, a sidebar, anywhere narrow; compactInput on <AgnoChat>
and <ChatWindow> switches their built-in composer to it:
<AgnoChat baseUrl="http://localhost:7777" mode="launcher" compactInput />
<ChatInput compact placeholder="Send a message" />When a product needs more from the composer — an @mention menu, context chips, a highlighted draft — it opens up rather than being replaced:
<ChatInput
value={draft} // own the draft
onValueChange={setDraft}
textareaRef={inputRef}
textareaProps={{
// reach the textarea itself
onKeyDown: (e) => {
if (menu.handles(e)) e.preventDefault()
}, // claims the key
role: 'combobox',
'aria-expanded': menuOpen
}}
renderTextarea={(props) => <HighlightedTextarea {...props} />} // or swap it
above={menuOpen && <MentionMenu />} // inside the dock, over the field
below={contextError && <p>{contextError}</p>}
allowFiles
accept=".md,.py,image/*" // dropped files obey this too
maxFiles={8}
maxFileSize={20 * 1024 * 1024}
onSend={async (text, files) => {
const ok = await prepare(text)
if (!ok) return false // keep the draft
chat.sendMessage(text, files ? { files } : undefined)
}}
/>A key handler in textareaProps runs before Enter-to-send; preventDefault()
there claims the key. Files can be dropped anywhere on the dock; the first
reason one was refused shows under the attachment chips (admitFiles is the
same check, exported).
Links
Every link the components render — in an answer, on a [1] chip, on a source
card — is a plain <a target="_blank">. Inside an app with a router, hand the
provider a linkComponent and in-app URLs stay client-side:
<ChatProvider linkComponent={DocsLink}>…</ChatProvider>
<AgnoChat linkComponent={DocsLink} />It receives href, children and the anchor attributes the component would
have set, and decides how to navigate — nothing sets target for it, so a
link opens in the same page unless the component says otherwise. The usual
shape routes your own origin through the router and pops everything else out:
import { Link } from 'react-router-dom'
import type { LinkComponent } from '@agno-hq/agent-os/chat'
const DocsLink: LinkComponent = ({ href, children, ...rest }) => {
const url = new URL(href, window.location.href)
return url.origin === window.location.origin ? (
<Link to={url.pathname + url.search + url.hash} {...rest}>
{children}
</Link>
) : (
<a href={href} target="_blank" rel="noreferrer noopener" {...rest}>
{children}
</a>
)
}Sources with no URL — retrieved knowledge-base chunks — are never links; their
[1] chip points at the card below the answer.
Run events
Every run keeps the events that produced it on its own message
(message.events) — live, and for runs restored from session history when the
agent stores them. <BehindTheScenes> builds its steps from those events the
way the AgentOS chat does: content deltas, model-request and reasoning-delta
events are dropped, started/completed pairs collapse into one step, and run
lifecycle events read as "Run Started" / "Run Completed" with the reported
duration. A message with no events falls back to what it does carry (tools,
reasoning, member turns, workflow steps).
The raw feed is not part of that panel. chat.events holds the latest run's
events, and every message keeps its own in message.events — render either
with <EventLog> wherever you want it:
<EventLog events={chat.events} maxHeight={240} />Citations, sources and link previews
Whatever a run cites — knowledge-base chunks in references, browsed links in
citations.urls — is flattened into one numbered list and rendered as cards
under the answer: the site's favicon and the title, linking to the source,
with the rest behind a "+N more" card past the first three. A [1] in the
answer text becomes a numbered disc pointing at card one, and hovering either
the disc or a link opens a preview with the URL and the cited passage:
<Message message={message} /> // markers, cards and previews, no props
<Message message={message} hideSources /> // just the answerThe hostname and favicon (/favicon.ico, then /favicon.svg) come from the
URL itself, and the title and passage
from the citation payload, so previews need no network of their own. To fill in
OpenGraph metadata, hand the provider a resolveLinkPreview — the browser can't
read another origin's meta tags, so back it with an endpoint of your own. It is
called once per URL, on first hover:
<ChatProvider
baseUrl="http://localhost:7777"
resolveLinkPreview={async (url) => {
const res = await fetch(`/api/og?url=${encodeURIComponent(url)}`)
return res.ok ? res.json() : null // { title, description, image, siteName, favicon }
}}
>
<ChatWindow />
</ChatProvider>Plenty of runs carry no references or citations at all — the model cites in
the answer itself, as numbered links, and lists the titles at the end:
Call `Agent.run()` in production. [1](https://docs.agno.com/agents/building-agents)
Sources: [Building Agents](https://docs.agno.com/agents/building-agents)That reads the same way: the numbered link becomes the chip, the labelled link names the card, and the numbering stays exactly as the model wrote it. Links in prose are left alone — only numbered ones count as citations.
Rendering sources yourself? collectSources(references, citations) and
sourcesFromMarkdown(content) are the same numbering the components use, and
<SourceCard>, <LinkPreviewCard> and <HoverPreview> are exported to build
with.
Follow-ups
An agent built with followups=True suggests what to ask next once it has
answered. They sit above the composer as "Related questions", one line each —
the full text is on hover — and clicking one sends it:
<AgnoChat baseUrl="http://localhost:7777" /> // on by default
<AgnoChat baseUrl="http://localhost:7777" hideFollowups /> // off
<AgnoChat baseUrl="http://localhost:7777" followupsTitle="Ask next" /> // your own headingOnly the latest answer offers them; earlier ones have been answered by the
conversation moving on. They arrive as a FollowupsCompleted event and are
stored on the run, so a reloaded session still shows them. To put them under
the answer instead, pass <Message onFollowup={chat.sendMessage}>; to place
them anywhere else, render <Followups> yourself from message.followups —
lines lets them wrap instead of clamping.
Markdown
Answers are rendered by Streamdown, which is built for text that arrives a token at a time: mid-stream it repairs the unterminated bold, the half-written link and the unclosed fence instead of showing you the raw characters. GFM comes with it — tables, task lists, strikethrough — along with a sanitising pass over the HTML.
Streamdown styles itself with Tailwind utility classes. Every element is mapped
back onto this package's own agno-md-* classes, so Tailwind is not
required and the shipped stylesheet and tokens still govern the look.
Fenced code blocks get a header naming the language, a line-number gutter and
syntax highlighting by Shiki. The highlighter loads on
the first fenced block, not before, and grammars load one at a time as
languages appear; a language Shiki doesn't know renders plain. Colours follow
the theme class like everything else. --agno-code-max-height (default 32rem)
caps a block before it scrolls.
The header also holds a copy button. Turn it off per-renderer or for a whole tree:
<Markdown content={answer} codeCopy={false} />
<ChatProvider codeCopy={false}>…</ChatProvider>
<AgnoChat codeCopy={false} />Message actions
A finished answer carries an action row — the AgentOS strip of small icon buttons under the text, shown when the message is pointed at. The package ships copy; the row is a slot for the rest:
<Message
message={m}
actions={<>
<button className="agno-msg__action" onClick={() => regenerate(m)} aria-label="Regenerate">
<RotateCw size={14} />
</button>
<button className="agno-msg__action" onClick={() => rate(m, 'up')} aria-label="Good answer">
<ThumbsUp size={14} />
</button>
</>}
/>
<Message message={m} hideActions /> // no row at all<MessageActions message={m}> is the row on its own, for a transcript you
render yourself, and <CopyButton text={…}> is the control inside it — the
same one a code block's header uses, so every copy affordance flashes the same
tick and falls back the same way when the Clipboard API is unavailable.
Prefer a different renderer? renderMarkdown replaces it entirely:
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
;<ChatWindow
chat={chat}
renderMarkdown={(c) => (
<ReactMarkdown remarkPlugins={[remarkGfm]}>{c}</ReactMarkdown>
)}
/>Note that streamdown is ESM-only, as is the remark/rehype ecosystem under it.
Bundlers resolve that fine; a bare require() of the CJS entry needs Node
22.12+.
Human-in-the-loop
When a run pauses for a tool confirmation or for user input, status becomes
'paused' and pendingRequirements / the paused message's tool_calls
describe what's needed. <ChatWindow> renders <HumanInput> automatically; to
build your own UI, call:
await chat.respondToConfirmation(true) // approve pending tool calls
await chat.respondToConfirmation(false) // reject
await chat.submitUserInput({ city: 'Lisbon' }) // fill input fields, then continueAgents and teams resume via the /continue endpoint with resolved tools;
workflows resume with step_requirements. The hook picks the right one based on
the selected entity type.
Session history
The hook tracks past sessions for the selected entity:
const chat = useAgnoChat({ baseUrl, entity })
chat.sessions // SessionEntry[]
chat.sessionsLoading // boolean
await chat.refreshSessions() // fetch the first page (20 sessions)
await chat.loadMoreSessions() // append the next page, while chat.hasMoreSessions
await chat.loadSession(sessionId) // load a transcript into the chat
await chat.renameSession(sessionId, 'Refund for order #4471')
await chat.deleteSession(sessionId)The list updates optimistically, as the AgentOS chat does: the moment a run
reports its session id, that session moves to the top of chat.sessions —
created on the spot, named after the message that started it, if the backend
just made it — rather than only appearing after the next refreshSessions().
A later refresh keeps the in-flight session even if the backend has not
persisted it yet.
Render them with <SessionList>:
<SessionList
sessions={chat.sessions}
activeSessionId={chat.sessionId}
streamingSessionIds={chat.streamingSessionIds}
loading={chat.sessionsLoading}
onSelect={chat.loadSession}
hasMore={chat.hasMoreSessions}
loadingMore={chat.sessionsLoadingMore}
onLoadMore={chat.loadMoreSessions}
onRename={chat.renameSession}
onDelete={chat.deleteSession}
onNew={chat.reset}
/>streamingSessionIds marks the sessions with a run in flight — each shows a
spinner in place of the delete button.
Switching session does not stop the run
Selecting another session (or starting a new chat) while a run is streaming
parks that run instead of cancelling it, as the AgentOS chat does: it keeps
streaming in the background, stays listed in chat.streamingSessionIds, and
loadSession restores it — mid-stream or finished — when you switch back,
rather than refetching a transcript that is still being written. Only
chat.cancel() stops a run.
The all-in-one widget shows this sidebar with showSessions:
<AgnoChat baseUrl="http://localhost:7777" showSessions />Lower-level API
import { AgnoClient, streamRun } from '@agno-hq/agent-os'
const client = new AgnoClient({
baseUrl: 'http://localhost:7777',
headers: { Authorization: 'Bearer …' }
})
await client.getEntities() // agents + teams + workflows
await client.getSessions('agent', 'agno_assist')
await client.getSessionRuns('agent', sessionId) // rehydrate history
await client.cancelRun('agent', 'agno_assist', runId)streamRun is the raw streaming primitive (parses the wire format and emits
normalised events) if you want to bypass the hook entirely.
Running the example
The example/ folder is a Vite app demonstrating the widget, the floating
launcher, every component, and the hook (with a live status panel + event log).
It imports the library from source.
cd example
npm install
npm run devThen open the app, set your AgentOS URL (default http://localhost:7777), and
pick an agent, team, or workflow.
Start an AgentOS first — see the Agno cookbooks under
cookbook/05_agent_os/.
CORS
The chat runs in the browser, so your AgentOS must allow the page's origin.
The example dev server is pinned to port 5173 (strictPort), so add that
origin when constructing AgentOS:
AgentOS(..., cors_allowed_origins=["http://localhost:5173"])A symptom of a CORS mismatch is an empty entity dropdown and a
Disallowed CORS origin response to the preflight request.
Notes
- Auth / headers — pass
headers(or a pre-builtclient) to send anAuthorizationheader on every request. - Sessions —
sessionIdis captured automatically on the first run. To restore history, callclient.getSessionRuns(...)and map runs intoChatMessage[], thenchat.setMessages(...). - Wire format — handles both AgentOS streaming shapes (the legacy flat event
objects and the
{ event, data }SSE envelope). - Dependencies — the library itself depends only on React (peer). The example additionally uses Vite.
Local development
git clone https://github.com/agno-agi/agno-react-components.git
cd agno-react-components
npm install
npm test # Node test runner
npm run typecheck # tsc --noEmit
npm run build # tsup -> dist/ (ESM + CJS + .d.ts)The published package is built with tsup into
dist/ (ESM index.js, CommonJS index.cjs, type declarations, and
styles.css). The example/ app, however, resolves the library straight from
src/ via a Vite alias, so you can develop against live changes without
rebuilding — see Running the example. prepublishOnly
runs the tests, typecheck, and build automatically, so npm publish always
ships a fresh dist/.
License
MIT © Agno
