@mcp-b/react-components 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/PromptInput-D6-G24p7.d.ts +528 -0
  2. package/dist/ThinkChatToolParts-DnUiZgCC.d.ts +83 -0
  3. package/dist/components/AgentChat.d.ts +11 -14
  4. package/dist/components/AgentChat.js +76 -57
  5. package/dist/components/Context.d.ts +6 -10
  6. package/dist/components/Context.js +4 -2
  7. package/dist/components/Conversation.d.ts +28 -28
  8. package/dist/components/Conversation.js +35 -54
  9. package/dist/components/Marker.d.ts +57 -0
  10. package/dist/components/Marker.js +89 -0
  11. package/dist/components/MessageScroller.d.ts +52 -0
  12. package/dist/components/MessageScroller.js +62 -0
  13. package/dist/components/PromptInput.d.ts +2 -534
  14. package/dist/components/PromptInput.js +17 -11
  15. package/dist/components/ThinkChat.d.ts +10 -77
  16. package/dist/components/ThinkChat.js +59 -856
  17. package/dist/components/ThinkChatActivity.d.ts +73 -0
  18. package/dist/components/ThinkChatActivity.js +429 -0
  19. package/dist/components/ThinkChatContext.d.ts +28 -0
  20. package/dist/components/ThinkChatContext.js +10 -0
  21. package/dist/components/ThinkChatToolPanels.d.ts +39 -0
  22. package/dist/components/ThinkChatToolPanels.js +355 -0
  23. package/dist/components/ThinkChatToolParts.d.ts +2 -0
  24. package/dist/components/ThinkChatToolParts.js +66 -0
  25. package/dist/components/WorkspaceExplorer.d.ts +1 -1
  26. package/dist/components/useStickToBottom.d.ts +5 -2
  27. package/dist/components/useStickToBottom.js +5 -2
  28. package/dist/styles/agent-chat.css +15 -3
  29. package/dist/styles/agent-connection.css +1 -0
  30. package/dist/styles/code-block.css +4 -4
  31. package/dist/styles/conversation.css +10 -0
  32. package/dist/styles/index.css +2 -0
  33. package/dist/styles/marker.css +95 -0
  34. package/dist/styles/message-scroller.css +90 -0
  35. package/dist/styles/message.css +2 -1
  36. package/dist/styles/scrollbars.css +10 -2
  37. package/dist/styles/streamdown.css +2 -0
  38. package/package.json +3 -2
@@ -2,15 +2,17 @@ import { t as cx } from "../class-names-9UtZwb0J.js";
2
2
  import { D as PlusIcon, O as RefreshIcon } from "../icons-BCNawx-T.js";
3
3
  import { Activity, ActivityContent, ActivityPanel, ActivityTrigger } from "./Activity.js";
4
4
  import { Context, ContextContent, ContextContentBody, ContextContentHeader, ContextTrigger, getContextUsage, getContextUsedTokens } from "./Context.js";
5
- import { Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton } from "./Conversation.js";
5
+ import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport } from "./MessageScroller.js";
6
+ import { ConversationEmptyState } from "./Conversation.js";
6
7
  import { Message, MessageAction, MessageActions, MessageAttachment, MessageContent, MessageResponse } from "./Message.js";
7
8
  import { MenuGroup, MenuGroupLabel } from "./Menu.js";
8
9
  import { PromptInput, PromptInputActionAddAttachments, PromptInputActionMenu, PromptInputActionMenuContent, PromptInputActionMenuTrigger, PromptInputApproval, PromptInputAttachments, PromptInputBody, PromptInputButton, PromptInputFooter, PromptInputPermissionSelector, PromptInputSubmit, PromptInputTextarea, PromptInputTools, getApprovalRequestId, getApprovalStateKey, getPendingToolApprovals, usePromptInputAttachments } from "./PromptInput.js";
9
10
  import { Reasoning, ReasoningContent, ReasoningTrigger } from "./Reasoning.js";
10
11
  import * as React from "react";
11
12
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
- import { getToolName, isFileUIPart, isReasoningUIPart, isTextUIPart, isToolOrDynamicToolUIPart } from "ai";
13
+ import { getToolName, isFileUIPart, isReasoningUIPart, isTextUIPart, isToolUIPart } from "ai";
13
14
  //#region src/components/AgentChat.tsx
15
+ const EMPTY_PENDING_APPROVALS = [];
14
16
  function AgentChatAttachButton() {
15
17
  const attachments = usePromptInputAttachments();
16
18
  return /* @__PURE__ */ jsx(PromptInputButton, {
@@ -33,11 +35,11 @@ function AgentChatAddMenu({ children }) {
33
35
  function isCompactedPart(message, part, partIndex) {
34
36
  const metadata = message.metadata;
35
37
  if (isTextUIPart(part)) return Array.isArray(metadata?.compactedTextParts) && metadata.compactedTextParts.includes(partIndex);
36
- if (isToolOrDynamicToolUIPart(part)) return Array.isArray(metadata?.compactedToolOutputs) && metadata.compactedToolOutputs.includes(part.toolCallId);
38
+ if (isToolUIPart(part)) return Array.isArray(metadata?.compactedToolOutputs) && metadata.compactedToolOutputs.includes(part.toolCallId);
37
39
  return false;
38
40
  }
39
41
  function getPartKey(part, partIndex) {
40
- if (isToolOrDynamicToolUIPart(part)) return part.toolCallId;
42
+ if (isToolUIPart(part)) return part.toolCallId;
41
43
  if ("id" in part && typeof part.id === "string") return `${part.type}-${part.id}`;
42
44
  return `${part.type}-${partIndex}`;
43
45
  }
@@ -88,39 +90,35 @@ function renderDefaultPart(part) {
88
90
  isStreaming: part.state === "streaming",
89
91
  children: [/* @__PURE__ */ jsx(ReasoningTrigger, {}), /* @__PURE__ */ jsx(ReasoningContent, { children: part.text })]
90
92
  });
91
- if (isToolOrDynamicToolUIPart(part)) return renderToolActivity(part);
93
+ if (isToolUIPart(part)) return renderToolActivity(part);
92
94
  return null;
93
95
  }
94
96
  /**
95
- * A complete agent chat surface: auto-scrolling conversation of AI SDK
96
- * `UIMessage`s (text, reasoning, and tool parts rendered by default) plus a
97
- * prompt composer. Customize per-part rendering with `renderPart`.
98
- *
99
- * This is the presentational layer under `ThinkChat` — props in, no
100
- * transport. Use it directly with plain AI SDK `useChat` or a read-only
101
- * transcript; if you have a Cloudflare Think agent, use `ThinkChat` instead.
97
+ * Low-level transcript/composer surface used by `ThinkChat`: props in, no
98
+ * transport. Product code that talks to a Cloudflare Think agent should render
99
+ * `ThinkChat`; use this directly only for plain AI SDK or read-only messages.
102
100
  */
103
- function AgentChat({ className, messages, status = "ready", onSubmit, onStop, placeholder = "Ask anything...", disabled, emptyState, composerTools, composerAddMenuItems, notice, onRegenerate, onApprovalResponse, executionApprovals, contextWindow, renderPart, renderMessageAfter, ref }) {
101
+ function AgentChat({ className, messages, status = "ready", onSubmit, onStop, placeholder = "Ask anything...", disabled, emptyState, composerTools, composerAddMenuItems, notice, onRegenerate, onApprovalResponse, executionApprovals, pendingApprovals: durableApprovals = EMPTY_PENDING_APPROVALS, contextWindow, renderPart, renderMessageAfter, ref }) {
104
102
  const handleSubmit = React.useCallback((message) => {
105
103
  onSubmit?.(message);
106
104
  }, [onSubmit]);
107
105
  const [skippedApprovalIds, setSkippedApprovalIds] = React.useState([]);
108
- const pendingApprovals = getPendingToolApprovals(messages, { pausedExecutions: Boolean(executionApprovals) }).filter(({ part }) => {
106
+ const pendingApprovalItems = [...getPendingToolApprovals(messages, { pausedExecutions: Boolean(executionApprovals) }).filter(({ part }) => {
109
107
  if (part.state === "approval-requested" && !onApprovalResponse) return false;
110
108
  const id = getApprovalStateKey(part);
111
109
  return Boolean(id) && !skippedApprovalIds.includes(id);
112
- });
110
+ }).map((approval) => ({ part: approval.part })), ...executionApprovals ? durableApprovals.filter((approval) => approval.source === "action" && !skippedApprovalIds.includes(approval.executionId)).map((approval) => ({ approval })) : []];
113
111
  const approvalsWired = Boolean(onApprovalResponse || executionApprovals);
114
112
  const [permissionMode, setPermissionMode] = React.useState("ask");
115
113
  const autoApprovedIds = React.useRef(/* @__PURE__ */ new Set());
116
114
  React.useEffect(() => {
117
115
  if (permissionMode !== "bypass") return;
118
- for (const { part } of pendingApprovals) {
119
- const id = getApprovalRequestId(part);
120
- const key = getApprovalStateKey(part);
116
+ for (const item of pendingApprovalItems) {
117
+ const id = "part" in item ? getApprovalRequestId(item.part) : item.approval.executionId;
118
+ const key = "part" in item ? getApprovalStateKey(item.part) : item.approval.executionId;
121
119
  if (!id || !key || autoApprovedIds.current.has(key)) continue;
122
120
  autoApprovedIds.current.add(key);
123
- if (part.state === "approval-requested") onApprovalResponse?.({
121
+ if ("part" in item && item.part.state === "approval-requested") onApprovalResponse?.({
124
122
  id,
125
123
  approved: true
126
124
  });
@@ -129,10 +127,11 @@ function AgentChat({ className, messages, status = "ready", onSubmit, onStop, pl
129
127
  }, [
130
128
  permissionMode,
131
129
  messages,
130
+ durableApprovals,
132
131
  onApprovalResponse,
133
132
  executionApprovals
134
133
  ]);
135
- const pendingApproval = permissionMode === "bypass" ? void 0 : pendingApprovals[0];
134
+ const pendingApproval = permissionMode === "bypass" ? void 0 : pendingApprovalItems[0];
136
135
  const contextUsage = contextWindow ? getContextUsage(messages) : void 0;
137
136
  const lastAssistantIndex = onRegenerate && status !== "streaming" && status !== "submitted" ? messages.findLastIndex((message) => message.role === "assistant") : -1;
138
137
  const composerRef = (el) => {
@@ -147,40 +146,59 @@ function AgentChat({ className, messages, status = "ready", onSubmit, onStop, pl
147
146
  return /* @__PURE__ */ jsxs("div", {
148
147
  ref,
149
148
  className: cx("agent-chat", className),
150
- children: [/* @__PURE__ */ jsxs(Conversation, {
151
- className: "agent-chat__conversation",
152
- children: [/* @__PURE__ */ jsxs(ConversationContent, { children: [messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsx(ConversationEmptyState, {
153
- title: "Start a conversation",
154
- description: "Ask the agent to inspect, plan, or build."
155
- }) : null, messages.map((message, messageIndex) => /* @__PURE__ */ jsxs(Message, {
156
- from: message.role,
157
- children: [
158
- message.parts.map((part, partIndex) => {
159
- const defaultRender = () => renderDefaultPart(part);
160
- const custom = renderPart?.({
161
- message,
162
- part,
163
- partIndex,
164
- defaultRender
165
- });
166
- const rendered = custom === void 0 ? defaultRender() : custom;
167
- return rendered ? /* @__PURE__ */ jsxs(MessageContent, { children: [rendered, isCompactedPart(message, part, partIndex) ? /* @__PURE__ */ jsx("div", {
168
- className: "message__compacted",
169
- children: "Output truncated for storage"
170
- }) : null] }, getPartKey(part, partIndex)) : null;
171
- }),
172
- renderMessageAfter?.({
173
- message,
174
- messageIndex
175
- }),
176
- onRegenerate && messageIndex === lastAssistantIndex ? /* @__PURE__ */ jsx(MessageActions, { children: /* @__PURE__ */ jsx(MessageAction, {
177
- label: "Retry",
178
- tooltip: "Regenerate response",
179
- onClick: () => onRegenerate(message.id),
180
- children: /* @__PURE__ */ jsx(RefreshIcon, { size: 14 })
181
- }) }) : null
182
- ]
183
- }, message.id ?? messageIndex))] }), /* @__PURE__ */ jsx(ConversationScrollButton, {})]
149
+ children: [/* @__PURE__ */ jsx(MessageScrollerProvider, {
150
+ autoScroll: true,
151
+ defaultScrollPosition: "last-anchor",
152
+ children: /* @__PURE__ */ jsxs(MessageScroller, {
153
+ className: "agent-chat__conversation",
154
+ children: [/* @__PURE__ */ jsx(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs(MessageScrollerContent, {
155
+ "aria-busy": status === "streaming" || status === "submitted",
156
+ className: "agent-chat__transcript-content",
157
+ children: [messages.length === 0 ? /* @__PURE__ */ jsx(MessageScrollerItem, {
158
+ className: "agent-chat__empty-row",
159
+ children: emptyState ?? /* @__PURE__ */ jsx(ConversationEmptyState, {
160
+ title: "Start a conversation",
161
+ description: "Ask the agent to inspect, plan, or build."
162
+ })
163
+ }) : null, messages.map((message, messageIndex) => /* @__PURE__ */ jsx(MessageScrollerItem, {
164
+ messageId: message.id,
165
+ scrollAnchor: message.role === "user",
166
+ children: /* @__PURE__ */ jsxs(Message, {
167
+ from: message.role,
168
+ children: [
169
+ message.parts.map((part, partIndex) => {
170
+ const defaultRender = () => renderDefaultPart(part);
171
+ const custom = renderPart?.({
172
+ message,
173
+ part,
174
+ partIndex,
175
+ defaultRender
176
+ });
177
+ const rendered = custom === void 0 ? defaultRender() : custom;
178
+ return rendered ? /* @__PURE__ */ jsxs(MessageContent, { children: [rendered, isCompactedPart(message, part, partIndex) ? /* @__PURE__ */ jsx("div", {
179
+ className: "message__compacted",
180
+ children: "Output truncated for storage"
181
+ }) : null] }, getPartKey(part, partIndex)) : null;
182
+ }),
183
+ renderMessageAfter?.({
184
+ message,
185
+ messageIndex
186
+ }),
187
+ onRegenerate && messageIndex === lastAssistantIndex ? /* @__PURE__ */ jsx(MessageActions, { children: /* @__PURE__ */ jsx(MessageAction, {
188
+ label: "Retry",
189
+ tooltip: "Regenerate response",
190
+ onClick: () => onRegenerate(message.id),
191
+ children: /* @__PURE__ */ jsx(RefreshIcon, { size: 14 })
192
+ }) }) : null
193
+ ]
194
+ })
195
+ }, message.id ?? messageIndex))]
196
+ }) }), /* @__PURE__ */ jsx(MessageScrollerButton, {
197
+ "aria-label": "Scroll to latest",
198
+ className: "agent-chat__scroll-button",
199
+ size: "icon"
200
+ })]
201
+ })
184
202
  }), /* @__PURE__ */ jsxs("div", {
185
203
  ref: composerRef,
186
204
  className: "agent-chat__composer",
@@ -191,12 +209,13 @@ function AgentChat({ className, messages, status = "ready", onSubmit, onStop, pl
191
209
  multiple: true,
192
210
  onSubmit: handleSubmit,
193
211
  children: pendingApproval ? /* @__PURE__ */ jsx(PromptInputApproval, {
194
- part: pendingApproval.part,
195
- count: pendingApprovals.length,
212
+ part: "part" in pendingApproval ? pendingApproval.part : void 0,
213
+ approval: "approval" in pendingApproval ? pendingApproval.approval : void 0,
214
+ count: pendingApprovalItems.length,
196
215
  onRespond: onApprovalResponse,
197
216
  executionApprovals,
198
217
  onSkip: () => {
199
- const id = getApprovalStateKey(pendingApproval.part);
218
+ const id = "part" in pendingApproval ? getApprovalStateKey(pendingApproval.part) : pendingApproval.approval.executionId;
200
219
  if (id) setSkippedApprovalIds((previous) => [...previous, id]);
201
220
  }
202
221
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -3,19 +3,13 @@ import * as React from "react";
3
3
  import { LanguageModelUsage, UIMessage } from "ai";
4
4
 
5
5
  //#region src/components/Context.d.ts
6
- /**
7
- * The AI SDK's `LanguageModelUsage`, taken whole. `Partial` only relaxes the
8
- * required-but-`undefined`able keys for fixtures/JSX literals — key names and
9
- * meanings are upstream's.
10
- */
11
- type ContextTokenUsage = Partial<LanguageModelUsage>;
12
6
  interface ContextProps extends React.HTMLAttributes<HTMLDivElement> {
13
7
  /** Tokens currently used. */
14
8
  usedTokens: number;
15
9
  /** Maximum context window size. */
16
10
  maxTokens: number;
17
11
  /** Optional usage breakdown. */
18
- usage?: ContextTokenUsage;
12
+ usage?: LanguageModelUsage;
19
13
  /** Optional model identifier shown in the header. */
20
14
  modelId?: string;
21
15
  /** Optional total cost in USD. */
@@ -31,11 +25,13 @@ interface ContextProps extends React.HTMLAttributes<HTMLDivElement> {
31
25
  *
32
26
  * @example
33
27
  * ```tsx
28
+ * const usage = getContextUsage(messages);
29
+ *
34
30
  * <Context
35
- * usedTokens={42_300}
31
+ * usedTokens={getContextUsedTokens(usage)}
36
32
  * maxTokens={200_000}
37
33
  * modelId="claude-opus-4-6"
38
- * usage={{ inputTokens: 30000, outputTokens: 12300 }}
34
+ * usage={usage}
39
35
  * totalCostUsd={0.42}
40
36
  * >
41
37
  * <ContextTrigger />
@@ -127,4 +123,4 @@ declare function getContextUsage(messages: readonly UIMessage[]): LanguageModelU
127
123
  /** Tokens occupying the context window: `totalTokens`, or input + output. */
128
124
  declare function getContextUsedTokens(usage: LanguageModelUsage | undefined): number;
129
125
  //#endregion
130
- export { Context, ContextCacheUsage, ContextCacheUsageProps, ContextContent, ContextContentBody, ContextContentBodyProps, ContextContentFooter, ContextContentFooterProps, ContextContentHeader, ContextContentHeaderProps, ContextContentProps, ContextInputUsage, ContextInputUsageProps, ContextOutputUsage, ContextOutputUsageProps, ContextProps, ContextReasoningUsage, ContextReasoningUsageProps, ContextTokenUsage, ContextTrigger, ContextTriggerProps, ContextUsageRowProps, getContextUsage, getContextUsedTokens };
126
+ export { Context, ContextCacheUsage, ContextCacheUsageProps, ContextContent, ContextContentBody, ContextContentBodyProps, ContextContentFooter, ContextContentFooterProps, ContextContentHeader, ContextContentHeaderProps, ContextContentProps, ContextInputUsage, ContextInputUsageProps, ContextOutputUsage, ContextOutputUsageProps, ContextProps, ContextReasoningUsage, ContextReasoningUsageProps, ContextTrigger, ContextTriggerProps, ContextUsageRowProps, getContextUsage, getContextUsedTokens };
@@ -17,11 +17,13 @@ function useContextContext() {
17
17
  *
18
18
  * @example
19
19
  * ```tsx
20
+ * const usage = getContextUsage(messages);
21
+ *
20
22
  * <Context
21
- * usedTokens={42_300}
23
+ * usedTokens={getContextUsedTokens(usage)}
22
24
  * maxTokens={200_000}
23
25
  * modelId="claude-opus-4-6"
24
- * usage={{ inputTokens: 30000, outputTokens: 12300 }}
26
+ * usage={usage}
25
27
  * totalCostUsd={0.42}
26
28
  * >
27
29
  * <ContextTrigger />
@@ -1,23 +1,15 @@
1
1
  import { t as Button } from "../Button-Ct5I6aTu.js";
2
+ import { MessageScrollerButtonProps, MessageScrollerItemProps } from "./MessageScroller.js";
2
3
  import * as React from "react";
4
+ import { UIMessage } from "ai";
3
5
 
4
6
  //#region src/components/Conversation.d.ts
5
- /**
6
- * Minimum message shape accepted by {@link ConversationDownload} and
7
- * {@link messagesToMarkdown}. Structurally compatible with the AI SDK
8
- * `UIMessage` type so consumers can pass those directly.
9
- */
10
- interface ConversationMessage {
7
+ /** AI SDK `UIMessage`, or a plain text fallback for non-chat exports. */
8
+ type ConversationMessage = UIMessage | {
11
9
  id?: string;
12
10
  role: string;
13
- /** AI SDK-style parts array. The text types are concatenated for export. */
14
- parts?: Array<{
15
- type: string;
16
- text?: string;
17
- }>;
18
- /** Plain-text content; used as a fallback when `parts` is not provided. */
19
11
  content?: string;
20
- }
12
+ };
21
13
  interface ConversationProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
22
14
  /** Additional class names for layout only; do not override scroll behavior from apps. */
23
15
  className?: string;
@@ -25,16 +17,17 @@ interface ConversationProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "
25
17
  children?: React.ReactNode;
26
18
  }
27
19
  /**
28
- * Scrollable container for a chat conversation. Sticks the viewport to the
29
- * bottom as new content arrives, unless the user has scrolled up. Adapted
30
- * from Vercel AI Elements and restyled with Sigvelo CSS tokens.
20
+ * Compatibility wrapper around {@link MessageScroller} for existing
21
+ * conversation imports. Prefer MessageScroller directly for new chat
22
+ * transcript surfaces.
31
23
  *
32
24
  * Use for assistant/user message streams. Do not use for static lists or
33
25
  * logs that should not auto-stick to the latest item.
34
26
  *
35
27
  * Sigvelo changes: removed Tailwind classes, uses a native scroll region with
36
- * the local stick-to-bottom hook, emits `role="log"` with polite updates, and
37
- * keeps transcript export in a separate ConversationDownload action.
28
+ * the shared MessageScroller primitive, emits `role="log"` with polite
29
+ * updates, and keeps transcript export in a separate ConversationDownload
30
+ * action.
38
31
  *
39
32
  * Styling consumes Sigvelo canvas, surface, border, spacing, radius, shadow,
40
33
  * and motion tokens. Add new behavior in this package when multiple apps need
@@ -48,7 +41,11 @@ interface ConversationProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "
48
41
  * ```tsx
49
42
  * <Conversation>
50
43
  * <ConversationContent>
51
- * {messages.map((m) => <Message key={m.id} {...m} />)}
44
+ * {messages.map((m) => (
45
+ * <ConversationItem key={m.id} messageId={m.id} scrollAnchor={m.role === "user"}>
46
+ * <Message {...m} />
47
+ * </ConversationItem>
48
+ * ))}
52
49
  * </ConversationContent>
53
50
  * <ConversationScrollButton />
54
51
  * </Conversation>
@@ -72,11 +69,17 @@ interface ConversationContentProps extends React.HTMLAttributes<HTMLDivElement>
72
69
  declare function ConversationContent({
73
70
  className,
74
71
  children,
75
- ref,
76
72
  ...props
77
73
  }: ConversationContentProps & {
78
74
  ref?: React.Ref<HTMLDivElement>;
79
75
  }): React.JSX.Element;
76
+ type ConversationItemProps = MessageScrollerItemProps;
77
+ /** Optional wrapper for one turn/message. Enables upstream scroll anchoring and visibility tracking. */
78
+ declare function ConversationItem({
79
+ className,
80
+ scrollAnchor,
81
+ ...props
82
+ }: ConversationItemProps): React.JSX.Element;
80
83
  interface ConversationEmptyStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
81
84
  title?: React.ReactNode;
82
85
  description?: React.ReactNode;
@@ -98,30 +101,27 @@ declare function ConversationEmptyState({
98
101
  }: ConversationEmptyStateProps & {
99
102
  ref?: React.Ref<HTMLDivElement>;
100
103
  }): React.JSX.Element;
101
- interface ConversationScrollButtonProps extends Omit<React.ComponentProps<typeof Button>, "onClick" | "className"> {
104
+ interface ConversationScrollButtonProps extends Omit<MessageScrollerButtonProps, "children" | "className"> {
102
105
  className?: string;
103
- /** Optional click handler; runs before the default scroll-to-bottom action. */
104
- onClick?: React.MouseEventHandler<HTMLButtonElement>;
105
106
  /** Accessible label for the button. */
106
107
  label?: string;
108
+ children?: React.ReactNode;
107
109
  }
108
110
  /**
109
111
  * Floating button that scrolls the conversation to the latest message.
110
- * Hidden while the viewport is already pinned to the bottom.
112
+ * Hidden from view and tab order while the viewport is already at the end.
111
113
  */
112
114
  declare function ConversationScrollButton({
113
115
  className,
114
116
  variant,
115
117
  color,
116
118
  size,
117
- onClick,
118
119
  label,
119
120
  children,
120
- ref,
121
121
  ...props
122
122
  }: ConversationScrollButtonProps & {
123
123
  ref?: React.Ref<HTMLButtonElement>;
124
- }): React.JSX.Element | null;
124
+ }): React.JSX.Element;
125
125
  /**
126
126
  * Converts a conversation to a Markdown document with a heading per message.
127
127
  *
@@ -163,4 +163,4 @@ declare function ConversationDownload({
163
163
  ref?: React.Ref<HTMLButtonElement>;
164
164
  }): React.JSX.Element | null;
165
165
  //#endregion
166
- export { Conversation, ConversationContent, ConversationContentProps, ConversationDownload, ConversationDownloadProps, ConversationEmptyState, ConversationEmptyStateProps, ConversationMessage, ConversationProps, ConversationScrollButton, ConversationScrollButtonProps, messagesToMarkdown };
166
+ export { Conversation, ConversationContent, ConversationContentProps, ConversationDownload, ConversationDownloadProps, ConversationEmptyState, ConversationEmptyStateProps, ConversationItem, ConversationItemProps, ConversationMessage, ConversationProps, ConversationScrollButton, ConversationScrollButtonProps, messagesToMarkdown };
@@ -1,27 +1,23 @@
1
1
  import { t as cx } from "../class-names-9UtZwb0J.js";
2
2
  import { d as DownloadIcon, t as ArrowDownIcon } from "../icons-BCNawx-T.js";
3
3
  import { Button } from "./Button.js";
4
- import { useStickToBottom } from "./useStickToBottom.js";
4
+ import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport } from "./MessageScroller.js";
5
5
  import * as React from "react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import { isReasoningUIPart, isTextUIPart } from "ai";
7
8
  //#region src/components/Conversation.tsx
8
- const ConversationContext = React.createContext(null);
9
- function useConversationContext(component) {
10
- const ctx = React.useContext(ConversationContext);
11
- if (!ctx) throw new Error(`${component} must be used inside a <Conversation>.`);
12
- return ctx;
13
- }
14
9
  /**
15
- * Scrollable container for a chat conversation. Sticks the viewport to the
16
- * bottom as new content arrives, unless the user has scrolled up. Adapted
17
- * from Vercel AI Elements and restyled with Sigvelo CSS tokens.
10
+ * Compatibility wrapper around {@link MessageScroller} for existing
11
+ * conversation imports. Prefer MessageScroller directly for new chat
12
+ * transcript surfaces.
18
13
  *
19
14
  * Use for assistant/user message streams. Do not use for static lists or
20
15
  * logs that should not auto-stick to the latest item.
21
16
  *
22
17
  * Sigvelo changes: removed Tailwind classes, uses a native scroll region with
23
- * the local stick-to-bottom hook, emits `role="log"` with polite updates, and
24
- * keeps transcript export in a separate ConversationDownload action.
18
+ * the shared MessageScroller primitive, emits `role="log"` with polite
19
+ * updates, and keeps transcript export in a separate ConversationDownload
20
+ * action.
25
21
  *
26
22
  * Styling consumes Sigvelo canvas, surface, border, spacing, radius, shadow,
27
23
  * and motion tokens. Add new behavior in this package when multiple apps need
@@ -35,7 +31,11 @@ function useConversationContext(component) {
35
31
  * ```tsx
36
32
  * <Conversation>
37
33
  * <ConversationContent>
38
- * {messages.map((m) => <Message key={m.id} {...m} />)}
34
+ * {messages.map((m) => (
35
+ * <ConversationItem key={m.id} messageId={m.id} scrollAnchor={m.role === "user"}>
36
+ * <Message {...m} />
37
+ * </ConversationItem>
38
+ * ))}
39
39
  * </ConversationContent>
40
40
  * <ConversationScrollButton />
41
41
  * </Conversation>
@@ -44,29 +44,19 @@ function useConversationContext(component) {
44
44
  * @see {@link https://elements.ai-sdk.dev/components/conversation | AI Elements Conversation}
45
45
  */
46
46
  function Conversation({ className, children, ref, ...props }) {
47
- const { viewportRef, contentRef, isAtBottom, scrollToBottom } = useStickToBottom();
48
47
  const handleViewportRef = React.useCallback((element) => {
49
- viewportRef(element);
50
48
  if (typeof ref === "function") {
51
49
  ref(element);
52
50
  return;
53
51
  }
54
52
  if (ref) ref.current = element;
55
- }, [ref, viewportRef]);
56
- const ctx = React.useMemo(() => ({
57
- isAtBottom,
58
- scrollToBottom,
59
- registerContentElement: contentRef
60
- }), [
61
- contentRef,
62
- isAtBottom,
63
- scrollToBottom
64
- ]);
65
- return /* @__PURE__ */ jsx(ConversationContext.Provider, {
66
- value: ctx,
67
- children: /* @__PURE__ */ jsx("div", {
53
+ }, [ref]);
54
+ return /* @__PURE__ */ jsx(MessageScrollerProvider, {
55
+ autoScroll: true,
56
+ defaultScrollPosition: "end",
57
+ children: /* @__PURE__ */ jsx(MessageScroller, {
68
58
  className: cx("conversation", className),
69
- children: /* @__PURE__ */ jsx("div", {
59
+ children: /* @__PURE__ */ jsx(MessageScrollerViewport, {
70
60
  ref: handleViewportRef,
71
61
  tabIndex: 0,
72
62
  className: "conversation__viewport",
@@ -80,25 +70,24 @@ function Conversation({ className, children, ref, ...props }) {
80
70
  * Inner layout wrapper for conversation children. Applies vertical stacking
81
71
  * and padding; does not scroll (the parent {@link Conversation} does).
82
72
  */
83
- function ConversationContent({ className, children, ref, ...props }) {
84
- const { registerContentElement } = useConversationContext("ConversationContent");
85
- return /* @__PURE__ */ jsx("div", {
86
- ref: React.useCallback((element) => {
87
- registerContentElement(element);
88
- if (typeof ref === "function") {
89
- ref(element);
90
- return;
91
- }
92
- if (ref) ref.current = element;
93
- }, [registerContentElement, ref]),
73
+ function ConversationContent({ className, children, ...props }) {
74
+ return /* @__PURE__ */ jsx(MessageScrollerContent, {
94
75
  role: "log",
95
76
  "aria-live": "polite",
96
- "aria-relevant": "additions text",
77
+ "aria-relevant": "additions",
97
78
  className: cx("conversation__content", className),
98
79
  ...props,
99
80
  children
100
81
  });
101
82
  }
83
+ /** Optional wrapper for one turn/message. Enables upstream scroll anchoring and visibility tracking. */
84
+ function ConversationItem({ className, scrollAnchor = false, ...props }) {
85
+ return /* @__PURE__ */ jsx(MessageScrollerItem, {
86
+ className: cx("conversation__item", className),
87
+ scrollAnchor,
88
+ ...props
89
+ });
90
+ }
102
91
  /**
103
92
  * Placeholder shown when there are no messages yet. Renders an icon, title,
104
93
  * and description stacked vertically; pass `children` to override the
@@ -128,23 +117,15 @@ function ConversationEmptyState({ className, title = "No messages yet", descript
128
117
  }
129
118
  /**
130
119
  * Floating button that scrolls the conversation to the latest message.
131
- * Hidden while the viewport is already pinned to the bottom.
120
+ * Hidden from view and tab order while the viewport is already at the end.
132
121
  */
133
- function ConversationScrollButton({ className, variant = "outline", color = "neutral", size = "icon", onClick, label = "Scroll to latest", children, ref, ...props }) {
134
- const { isAtBottom, scrollToBottom } = useConversationContext("ConversationScrollButton");
135
- if (isAtBottom) return null;
136
- return /* @__PURE__ */ jsx(Button, {
137
- ref,
138
- type: "button",
122
+ function ConversationScrollButton({ className, variant = "outline", color = "neutral", size = "icon", label = "Scroll to latest", children, ...props }) {
123
+ return /* @__PURE__ */ jsx(MessageScrollerButton, {
139
124
  variant,
140
125
  color,
141
126
  size,
142
127
  className: cx("conversation__scroll-button", className),
143
128
  "aria-label": label,
144
- onClick: (event) => {
145
- onClick?.(event);
146
- if (!event.defaultPrevented) scrollToBottom();
147
- },
148
129
  ...props,
149
130
  children: children ?? /* @__PURE__ */ jsx(ArrowDownIcon, {})
150
131
  });
@@ -156,7 +137,7 @@ const ROLE_HEADING = {
156
137
  tool: "### Tool"
157
138
  };
158
139
  function defaultFormatMessage(message) {
159
- return `${ROLE_HEADING[message.role] ?? `### ${message.role}`}\n\n${(message.parts ? message.parts.filter((part) => part.type === "text" || part.type === "reasoning").map((part) => part.text ?? "").join("") : message.content ?? "").trim()}`;
140
+ return `${ROLE_HEADING[message.role] ?? `### ${message.role}`}\n\n${("parts" in message ? message.parts.filter((part) => isTextUIPart(part) || isReasoningUIPart(part)).map((part) => part.text).join("") : message.content ?? "").trim()}`;
160
141
  }
161
142
  /**
162
143
  * Converts a conversation to a Markdown document with a heading per message.
@@ -202,4 +183,4 @@ function ConversationDownload({ className, messages, filename = "conversation.md
202
183
  });
203
184
  }
204
185
  //#endregion
205
- export { Conversation, ConversationContent, ConversationDownload, ConversationEmptyState, ConversationScrollButton, messagesToMarkdown };
186
+ export { Conversation, ConversationContent, ConversationDownload, ConversationEmptyState, ConversationItem, ConversationScrollButton, messagesToMarkdown };
@@ -0,0 +1,57 @@
1
+ import * as React from "react";
2
+
3
+ //#region src/components/Marker.d.ts
4
+ type MarkerVariant = "default" | "separator" | "border";
5
+ type MarkerRenderProps = React.HTMLAttributes<HTMLElement> & {
6
+ ref?: React.Ref<HTMLElement>;
7
+ };
8
+ type MarkerRenderState = {
9
+ variant: MarkerVariant;
10
+ };
11
+ type MarkerRenderFunction = (props: MarkerRenderProps, state: MarkerRenderState) => React.ReactElement | null;
12
+ type MarkerRender = React.ReactElement<React.HTMLAttributes<HTMLElement>> | MarkerRenderFunction;
13
+ interface MarkerProps extends React.HTMLAttributes<HTMLDivElement> {
14
+ /** Visual treatment for status, separator, or bordered marker rows. */
15
+ variant?: MarkerVariant;
16
+ /** Replace the root element while keeping marker classes, refs, and handlers merged. */
17
+ render?: MarkerRender;
18
+ }
19
+ interface MarkerVariantsOptions {
20
+ variant?: MarkerVariant | null;
21
+ className?: string;
22
+ }
23
+ declare function markerVariants({
24
+ variant,
25
+ className
26
+ }?: MarkerVariantsOptions): string;
27
+ /**
28
+ * Compact status or separator row for transcript events like compaction,
29
+ * checkpoints, and progress markers.
30
+ */
31
+ declare function Marker({
32
+ className,
33
+ variant,
34
+ render,
35
+ ref,
36
+ ...props
37
+ }: MarkerProps & {
38
+ ref?: React.Ref<HTMLDivElement>;
39
+ }): React.JSX.Element | null;
40
+ interface MarkerIconProps extends React.HTMLAttributes<HTMLSpanElement> {}
41
+ declare function MarkerIcon({
42
+ className,
43
+ ref,
44
+ ...props
45
+ }: MarkerIconProps & {
46
+ ref?: React.Ref<HTMLSpanElement>;
47
+ }): React.JSX.Element;
48
+ interface MarkerContentProps extends React.HTMLAttributes<HTMLSpanElement> {}
49
+ declare function MarkerContent({
50
+ className,
51
+ ref,
52
+ ...props
53
+ }: MarkerContentProps & {
54
+ ref?: React.Ref<HTMLSpanElement>;
55
+ }): React.JSX.Element;
56
+ //#endregion
57
+ export { Marker, MarkerContent, MarkerContentProps, MarkerIcon, MarkerIconProps, MarkerProps, MarkerVariant, MarkerVariantsOptions, markerVariants };