@cubos/agent-sdk-react-dom

中文文档
0.0.1174337发布于 2026/11/07SEE LICENSE IN LICENSETypeScriptES Module近 30 天下载 1.1万

Ready-made React components for Cubos Agent chats: agent markdown with code, tables and TeX. Styled out of the box.

原始使用文档
文档目录8 节
  1. Agent markdown
  2. The CSS takes care of itself
  3. Making it yours
  4. Two things worth knowing
  5. Voice messages
  6. Recording a voice message
  7. Keeping the transcript anchored
  8. What belongs in this package

@cubos/agent-sdk-react-dom

Ready-made React components for a Cubos Agent chat. Everything here renders DOM — that is the whole difference from @cubos/agent-sdk-react, which stays platform-neutral so its hooks also run in React Native.

bun add @cubos/agent-sdk-react-dom    # or npm / pnpm / yarn

Its dependencies are the markdown chain and lucide-react for the handful of icons a player needs. React is a peer.

Agent markdown

import { AgentMarkdown } from "@cubos/agent-sdk-react-dom";

<AgentMarkdown>{message.content}</AgentMarkdown>;

That is the whole setup. GFM tables, fenced code with syntax highlighting, TeX, and a look that already works — no stylesheet to import.

The CSS takes care of itself

The components import their own stylesheet. Your bundler resolves that the way it resolves any other import — Vite, Next, Rspack, webpack, Bun — and there is nothing for you to remember, no order to get right, no styles.css to hunt for in a README.

Two properties make that safe to do in someone else's app:

  • Nothing escapes. Every selector is scoped to a class this package puts on its own wrapper: .cubos-agent-sdk-react-dom for what every component here shares, .cubos-agent-sdk-react-dom--markdown for this one's rules. The sheet cannot touch anything it didn't draw.
  • Nothing wins an argument. The whole sheet sits in the cubos-agent-sdk-react-dom cascade layer, and every selector is wrapped in :where(), which contributes no specificity. A layer loses to unlayered CSS outright, and a bare p {} beats a zero-specificity rule — so these are defaults, and yours are decisions.

If your app puts its own CSS in layers (Tailwind v4 does), name ours once so the order is explicit rather than accidental:

/* Above your `@import`, since that is where a layer's position gets fixed. */
@layer theme, base, cubos-agent-sdk-react-dom, components, utilities;

@import "tailwindcss";

After your reset, before your own classes. A reset clears borders and margins on *; below it, these defaults would be erased along with the browser's. Above your components and utilities, and your app would have to fight this package to restyle anything.

Theming is --cubos-agent-sdk-react-dom-*, on the package class — one block, and every component in here follows it:

.cubos-agent-sdk-react-dom {
  --cubos-agent-sdk-react-dom-border: var(--my-border);
  --cubos-agent-sdk-react-dom-surface: var(--my-code-bg);
  --cubos-agent-sdk-react-dom-keyword: var(--my-pink);
}

Most of it you never touch: borders, muted text and code surfaces are mixed from currentColor, so the same sheet reads correctly on a white page, a dark one, or inside a coloured bubble. Only the six syntax colours are absolute, and they switch on prefers-color-scheme — if your app toggles dark mode itself, override those under whatever selector you use.

The same tokens are a prop, for an app that themes in JavaScript or wants one instance to differ from the rest:

<VoiceMessage colors={{ surface: "#0b57d0", border: "#8ab4f8" }} … />

They land as inline custom properties, which is the app speaking directly — it outranks every stylesheet, this package's included, without an !important anywhere.

Making it yours

// Your design system, element by element. Anything you style here outranks the
// packaged default for that element; anything you leave alone keeps it.
<AgentMarkdown components={{ p: (props) => <p className="my-2" {...props} /> }}>
  {text}
</AgentMarkdown>;

components is react-markdown's map, merged over the defaults — so overriding a leaves the table scroller in place. gfm, math and highlight each turn their plugin off. That is the whole API: there is no unstyled flag, because with a scoped layer at zero specificity there is nothing to opt out of.

Two things worth knowing

Raw HTML is never rendered, and there is no option to allow it. Message content is model output, which is untrusted input however friendly the agent. rehype-raw is deliberately absent: markdown in, elements out.

KaTeX is yours: it renders with your katex, and its stylesheet is your import. katex is a peer dependency, so the copy that writes the markup is the copy whose CSS styles it — KaTeX's stylesheet is written for its own release's markup, and two copies drifting apart mis-style formulas with nothing failing. The stylesheet is not bundled either: it ships fonts, and re-exporting a copy of those from here would double them in your bundle. Add it once in your app:

bun add katex
import "katex/dist/katex.min.css";

Without the stylesheet formulas still render, just unstyled. math={false} if the agent never writes any.

Voice messages

import { VoiceMessage } from "@cubos/agent-sdk-react-dom";

<VoiceMessage
  loadBytes={(signal) => client.fetchAttachment(conversationId, message.id, attachment.id, signal)}
  bytes={attachment.bytes}
  transcript={message.content}
/>;

It fills the width of whatever holds it — how wide a voice message is, is the bubble's decision, not the player's. All it insists on is a floor, so a container with no width of its own doesn't collapse around a waveform that is absolutely positioned.

Play/pause, a seek bar, the elapsed time, and the transcription folded away under a disclosure — collapsed by default, because the player is the message and the words are there when you want them. defaultTranscriptOpen starts it open.

Three things it does that are easy to miss:

  • The waveform is the clip's own. It is read off the decoded samples — peak amplitude per bucket, normalised — so a pause in the recording is a gap on screen. Until the bytes are here there is nothing to read, and the track is a plain progress bar rather than an invented pattern. A codec the browser can't decode keeps that bar; it never breaks playback.
  • Nothing is fetched until you press play. A transcript often answers the question, and a conversation full of voice notes should not pull every clip down to render one screen.
  • The seek control is a real <input type="range">, for the keyboard and the ARIA — painted by us, because the native thumb needs a vendor pseudo-element and hiding it with opacity loses to Tailwind's preflight, which sets opacity: 1 on every input from a layer above this package's.
  • It measures a MediaRecorder clip. Those carry no duration in their header, so browsers report Infinity until the file has been seeked through; the component forces that measurement and rewinds, which is why the seek bar has a range at all.

loadBytes returns a Blob because the bytes usually sit behind a bearer token, which an <audio src> cannot carry. Every string is in labels:

<VoiceMessage
  loadBytes={…}
  labels={{ play: "Tocar", pause: "Pausar", transcript: "Transcrição" }}
/>

Recording a voice message

useRecorder is the other half of VoiceMessage: capture, as a Blob ready for sendAudio.

const rec = useRecorder();

{
  rec.state !== "unsupported" && (
    <button onClick={() => (rec.state === "recording" ? finish() : rec.start())}>
      {rec.state === "recording" ? `Gravando ${rec.seconds}s` : "Gravar"}
    </button>
  );
}

async function finish() {
  const clip = await rec.stop(); // null when nothing was captured
  if (clip) await sendAudio(clip);
}

Two things it owns, both silent when wrong:

  • The container is probed, not chosen. Chrome and Firefox record WebM; Safari only ever produces MP4/AAC and fails silently when asked for WebM — a clip that is simply never assembled.
  • The stream is released on every exit path — stop, cancel, unmount, a failed permission. A live MediaStream keeps the browser's recording indicator lit long after the UI has moved on, with nothing on screen to explain it.

state is unsupported where there is no MediaRecorder or no secure context (getUserMedia exists only in one), and it settles after mount rather than during render, so it is safe to render on a server. cancel() drops the take without assembling it; error carries a denied microphone, which is the failure worth putting in front of someone.

Keeping the transcript anchored

useTranscriptAnchor is the scroll behaviour of a bottom-anchored transcript with none of its looks: it renders no element, sets no class, and hands you two refs.

const { scrollRef, contentRef, onScroll, isAtBottom, scrollToEnd } = useTranscriptAnchor({
  tailKey: messages.at(-1)?.id,
  hasOlder,
  loadOlder,
});

<div ref={scrollRef} onScroll={onScroll} className="overflow-auto">
  <div ref={contentRef}>{items}</div>
</div>;
{
  !isAtBottom && <button onClick={scrollToEnd}>Ver o mais recente</button>;
}

tailKey is anything that changes when the tail grows — the last event's seq, the message count, whether a typing indicator is up. It is only a trigger; the value is never read. loadOlder resolves with how many items it prepended, which is what lets a page that came back empty release the anchor instead of freezing the viewport.

Four behaviours, each of which was a bug in this repository before it was a rule:

  1. Follow only if already following. The intent is recorded when the reader scrolls, never measured after an update — by then the content has grown, so someone sitting exactly at the end is suddenly "far" from it by the height of whatever just arrived.
  2. Follow when the content grows, not when React re-renders. A transcript grows for reasons no state changed: an image decodes, a font swaps, a disclosure opens, a card lays out a frame late. That is what contentRef watches, and skipping it leaves the last few hundred pixels — the ones the answer is written in — below the fold.
  3. Hold the reader's place when paging backwards, before paint, against a height measured before the commit. The correction survives the renders that happen while the page is in flight; consuming it on the first of them is what drops the reader at the top of a page they never asked to jump to.
  4. Keep pulling while the content is shorter than the viewport, or a short first page leaves a box that cannot scroll, and no scroll event ever fires to ask for the rest.

What belongs in this package

The bar is deliberately high, because a components package that answers every request becomes a chat UI nobody can restyle:

  1. It solves a problem of the protocol — markdown the model wrote, bytes behind a bearer token, a clip that needs a player — or it is a mechanic that is invisible when right and wrong in the same way everywhere, like a transcript that has to follow the answer being written into it. Never a question of product taste.
  2. There is one obviously right behaviour, or a security invariant worth keeping in one place.
  3. Styling is delegated, not baked in.
  4. It was already written twice.

Bubbles, composers and conversation lists fail (1): they are where products differ most, and the dashboard's packages/web/components/chats/ in the repository is the worked example to copy instead. Recording a voice note is the next candidate — the dashboard has it, and the question is only whether a recorder belongs next to a player.

版本与发布记录

@cubos/agent-sdk-react-dom 共发布 113 个版本,首次发布于 2026/09/08;过去 12 个月发布了 113 个版本。

版本发布日期距今
0.0.1174337latest2026/11/073 天前
0.0.11725352026/11/055 天前
0.0.11717452026/11/046 天前
0.0.11714852026/11/046 天前
0.0.11712492026/11/047 天前
0.0.11704982026/11/037 天前

关于 @cubos/agent-sdk-react-dom 的常见问题

@cubos/agent-sdk-react-dom 是什么?

Ready-made React components for Cubos Agent chats: agent markdown with code, tables and TeX. Styled out of the box.

如何安装 @cubos/agent-sdk-react-dom?

在项目目录运行 npm install @cubos/agent-sdk-react-dom,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:katex(>=0.16.11)、react(>=18)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@cubos/agent-sdk-react-dom 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cubos/agent-sdk-react-dom 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cubos/agent-sdk-react-dom 可以商用吗?

@cubos/agent-sdk-react-dom 使用 SEE LICENSE IN LICENSE 许可。许可条款写在包内的独立文件中,请阅读该文件确认可否商用。以上为概括,不构成法律意见,请以许可原文为准。

@cubos/agent-sdk-react-dom 的下载量有多少?

根据 npm 官方下载统计,@cubos/agent-sdk-react-dom 在 2026/10/11 – 2026/11/09 这 30 天内被下载 10,635 次,日均约 355 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cubos/agent-sdk-react-dom 还在维护吗?

最新版本 0.0.1174337 发布于 2026/11/07(3 天前);至今共发布 113 个版本,其中过去 12 个月发布了 113 个;首次发布于 2026/09/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。