npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vinyasa/ai-native

v2.0.3

Published

24 UI patterns for building AI products: chat threads, streaming responses, tool calls, agent timelines, diff review, workflow graphs, and the surrounding chrome (composer, citations, model picker, token usage, conversation list).

Downloads

552

Readme

@vinyasa/ai-native

24 UI patterns for building AI products: chat threads, streaming responses, tool calls, agent timelines, diff review, workflow graphs, and the surrounding chrome (composer, citations, model picker, token usage, conversation list).

Installation

pnpm add @vinyasa/ai-native @vinyasa/button @vinyasa/feedback @vinyasa/icons @vinyasa/layout @vinyasa/overlay @vinyasa/tokens @vinyasa/typography @tiptap/core @tiptap/extension-document @tiptap/extension-hard-break @tiptap/extension-paragraph @tiptap/extension-text @tiptap/extensions @tiptap/pm @tiptap/react @tiptap/suggestion @xyflow/react cmdk react react-dom

This package composes existing @vinyasa/* primitives rather than reimplementing them — Button for action slots, Dialog/Popover/DropdownMenu from @vinyasa/overlay, ProgressBar from @vinyasa/feedback, MarkdownRenderer/Text/Code/Link from @vinyasa/typography, Box/Flex from @vinyasa/layout, and icons from @vinyasa/icons — all seven are peer dependencies, and rendering requires a VinyasaProvider (from @vinyasa/tokens) above them in the tree, same as everywhere else.

The remaining peers exist for one component each:

  • @tiptap/* (core, extension-document/-hard-break/-paragraph/-text, extensions, pm, react, suggestion) — PromptEditor builds a deliberately minimal schema (text and hard breaks only, no headings/marks/lists) rather than depending on @vinyasa/form's full-StarterKit RichTextEditor, which doesn't fit a single-line chat composer. @tiptap/suggestion powers its "/" slash-command popup.
  • @xyflow/react — WorkflowBuilder wraps its ReactFlow directly, with custom themed node/edge components.
  • cmdk — backs the slash-command popup inside PromptEditor, the same list primitive @vinyasa/form's Combobox and @vinyasa/overlay's CommandPalette already use.

No separate stylesheet import is needed — each component's CSS is bundled into its own JS entry.

This package has no root export — every component is subpath-only (import { ChatThread } from '@vinyasa/ai-native/chat-thread', never from '@vinyasa/ai-native'). A root barrel re-exporting all 24 components would let a bundler tree-shake the unused JS down to just the ones you import, but the CSS side-effect imports the others carry are not eligible for the same tree-shaking (confirmed empirically with both esbuild and Rollup on this monorepo's other packages). Removing the root entry entirely makes that the only possible outcome, not something that depends on your bundler being clever enough to shake it out.

Composition

Unlike @vinyasa/overlay/@vinyasa/form, none of these 24 components expose a compound .Root/.Trigger/... namespace — each is a single flat-prop component. Several are meant to be composed into another component's slot rather than used standalone: ThinkingIndicator, StreamingResponse, and ToolCallCard typically fill a ChatThread message's content; MessageActions and MessageVersionPager typically fill its actions. AssistantPanel is the batteries-included composite that bundles ChatThread + PromptEditor (+ StreamingResponse) into one ready-to-drop-in widget — reach for ChatThread/PromptEditor directly only if you need a layout AssistantPanel doesn't offer.

Usage

ChatThread

The core message-list layout for a chat UI — user/assistant bubbles, with assistant string content rendered through @vinyasa/typography's MarkdownRenderer.

import { ChatThread } from '@vinyasa/ai-native/chat-thread';

<ChatThread
	messages={[
		{ id: '1', role: 'user', content: 'Summarize this PR.' },
		{
			id: '2',
			role: 'assistant',
			content: '**Summary:** adds retry logic.',
			actions: <MessageActions copyText="..." />,
		},
	]}
/>;

| Prop | Type | Default | Description | | ---------------- | --------------------- | ------------- | ----------------------------------------------------------------------------------------- | | messages | ChatThreadMessage[] | — | Required. { id; role: 'user' \| 'assistant'; content: ReactNode; actions?: ReactNode }. | | userLabel | string | 'You' | | | assistantLabel | string | 'Assistant' | |

content accepts any ReactNode — pass a markdown string, or compose in StreamingResponse/ThinkingIndicator/ToolCallCard for an in-progress or tool-augmented message.

PromptEditor

The chat composer — a Tiptap-backed field with Enter-to-submit, Shift+Enter for a newline, {{variable}} highlighting, a "/" slash-command menu, attachments, and an auto inline-vs-stacked layout.

import { PromptEditor } from '@vinyasa/ai-native/prompt-editor';
import Button from '@vinyasa/button/button';

<PromptEditor
	value={draft}
	onValueChange={setDraft}
	onSubmit={handleSend}
	placeholder="Ask anything..."
	attachments={attachments}
	onRemoveAttachment={removeAttachment}
	commands={[{ id: 'explain', label: '/explain', description: 'Explain the selected code' }]}
	trailing={
		<Button size="sm" onClick={() => handleSend(draft)}>
			Send
		</Button>
	}
/>;

| Prop | Type | Default | Description | | ------------------------- | --------------------------------- | -------- | ---------------------------------------------------------------------------------------------- | | value | string | — | Required. | | onValueChange | (value: string) => void | — | Required. | | onSubmit | (value: string) => void | — | Fires on Enter without Shift, when non-blank. | | disabled/isSubmitting | boolean | false | | | leading/trailing | ReactNode | — | Docked bottom-left/right once the layout is stacked. | | layout | 'auto' \| 'inline' \| 'stacked' | 'auto' | | | attachments | PromptEditorAttachment[] | — | { id; name; size?; previewUrl? }. Pair with onRemoveAttachment. | | radius | 'sm' \| 'md' \| 'full' | 'sm' | | | highlightVariables | boolean | true | Pair with variablePattern (default /\{\{[^{}]+\}\}/g). | | commands | PromptEditorCommand[] | — | Enables the "/" menu. { id; label; description?; icon?; keywords?; insertText?; onSelect? }. | | maxLength | number | — | Enables a live character counter. |

StreamingResponse

Renders an assistant message's text with a gradual reveal animation as new chunks of text arrive — a token-by-token feel, backed by markdown rendering.

import { StreamingResponse } from '@vinyasa/ai-native/streaming-response';

<StreamingResponse text={partialAnswer} isStreaming={!isDone} />;

| Prop | Type | Default | Description | | --------------------- | --------- | ------- | -------------------------------------------------------- | | text | string | — | Required. Full text so far — update it as chunks arrive. | | isStreaming | boolean | true | | | charactersPerSecond | number | 40 | Reveal speed. |

ThinkingIndicator

A pre-stream "assistant is composing" affordance (label + animated dots) — compose it as a ChatThread message's content before real tokens arrive, then swap it for StreamingResponse.

import { ChatThread } from '@vinyasa/ai-native/chat-thread';
import { ThinkingIndicator } from '@vinyasa/ai-native/thinking-indicator';

<ChatThread messages={[{ id: 't', role: 'assistant', content: <ThinkingIndicator /> }]} />;

| Prop | Type | Default | Description | | ------- | ----------- | ------------ | -------------------------------------------------------------------- | | label | ReactNode | 'Thinking' | Also the role="status" accessible name. Pass null for dots only. |

ReasoningInspector

A collapsed-by-default disclosure for an assistant's chain-of-thought, matching the "thinking block" UX pattern.

import { ReasoningInspector } from '@vinyasa/ai-native/reasoning-inspector';

<ReasoningInspector defaultOpen={false}>
	{'The user wants X, so I should first check Y...'}
</ReasoningInspector>;

| Prop | Type | Default | Description | | ------------- | ----------- | ------------- | ---------------------------------------------------------------------------- | | children | ReactNode | — | Required. Markdown string, or StreamingResponse for in-progress reasoning. | | open | boolean | uncontrolled | | | defaultOpen | boolean | false | | | label | string | 'Reasoning' | |

ToolCallCard

An inline, collapsible card for a single tool invocation shown inside a chat message — distinct from AgentTimeline's standalone multi-event log and ExecutionConsole's standalone terminal.

import { ToolCallCard } from '@vinyasa/ai-native/tool-call-card';

<ToolCallCard
	toolName="search_docs"
	status="success"
	args={'{"query":"useEffect"}'}
	result={'3 results found'}
/>;

| Prop | Type | Default | Description | | ---------- | ------------------------------------------------ | ------- | ------------------------------------------------------------------ | | toolName | string | — | Required. | | status | 'pending' \| 'running' \| 'success' \| 'error' | — | Required. | | args | ReactNode | — | String renders as a code block via @vinyasa/typography's Code. | | result | ReactNode | — | Same rendering as args. |

The trigger is disabled (non-expandable) when neither args nor result is given.

MessageActions

A row of small per-message actions — copy, thumbs up/down, regenerate, edit — that only renders the buttons whose handler or data is actually supplied.

import { MessageActions } from '@vinyasa/ai-native/message-actions';

<MessageActions
	copyText={message.content}
	feedback={feedback}
	onFeedback={setFeedback}
	onRegenerate={regenerate}
/>;

| Prop | Type | Default | Description | | -------------- | ------------------------ | ------- | ------------------------------------------------------------------------------------- | | copyText | string | — | Renders the copy button; self-manages clipboard write + a 1.5s "Copied" confirmation. | | feedback | 'up' \| 'down' \| null | — | Pair with onFeedback — both are required to show the thumbs buttons. | | onRegenerate | () => void | — | | | onEdit | () => void | — | |

MessageVersionPager

A "◀ 2/3 ▶" pager for stepping through an edited or regenerated message's versions — the caller owns the version array and which one is current.

import { MessageVersionPager } from '@vinyasa/ai-native/message-version-pager';

<MessageVersionPager
	index={version}
	count={versions.length}
	onPrevious={() => setVersion((v) => v - 1)}
	onNext={() => setVersion((v) => v + 1)}
/>;

| Prop | Type | Default | Description | | --------------------- | ------------ | ------- | ------------------ | | index | number | — | Required. 0-based. | | count | number | — | Required. | | onPrevious/onNext | () => void | — | Required. |

AssistantEmptyState

A "How can I help?" empty state for the assistant panel, with optional example prompts — pass it as <AssistantPanel emptyMessage={<AssistantEmptyState ... />}>.

import { AssistantEmptyState } from '@vinyasa/ai-native/assistant-empty-state';

<AssistantEmptyState
	description="Ask about your codebase, or try one of these:"
	examples={[{ key: 'explain', label: 'Explain this file', onClick: explainFile }]}
/>;

| Prop | Type | Default | Description | | ------------- | -------------------- | ------------------- | --------------------------------------------------------------- | | title | ReactNode | 'How can I help?' | | | description | ReactNode | — | | | examples | ActionSuggestion[] | — | Same shape as ActionSuggestions, rendered via that component. |

ActionSuggestions

A wrapping row of AI-suggested quick-follow-up actions ("Run tests", "View diff") shown after an assistant response, built on @vinyasa/button's outline variant.

import { ActionSuggestions } from '@vinyasa/ai-native/action-suggestions';

<ActionSuggestions
	actions={[
		{ key: 'run-tests', label: 'Run tests', onClick: runTests },
		{ key: 'view-diff', label: 'View diff', onClick: viewDiff },
	]}
/>;

| Prop | Type | Default | Description | | --------- | -------------------- | ------- | ------------------------------------------------------- | | actions | ActionSuggestion[] | — | Required. { key; label; icon?; disabled?; onClick? }. | | size | ButtonSize | 'sm' | |

PromptHistory

A simple click-to-reuse list of past prompts with relative timestamps ("2 hours ago").

import { PromptHistory } from '@vinyasa/ai-native/prompt-history';

<PromptHistory entries={history} onSelect={(entry) => setDraft(entry.text)} />;

| Prop | Type | Default | Description | | -------------- | ---------------------- | ------------------- | ------------------------------------ | | entries | PromptHistoryEntry[] | — | Required. { id; text; timestamp }. | | onSelect | (entry) => void | — | Required. | | emptyMessage | ReactNode | 'No prompts yet.' | |

ConversationList

A sidebar list of past conversations (not messages), grouped into day buckets (Pinned, Today, Yesterday, Previous 7 Days, Older), with a per-row rename/delete/pin menu built on @vinyasa/overlay's DropdownMenu.

import { ConversationList } from '@vinyasa/ai-native/conversation-list';

<ConversationList
	conversations={conversations}
	activeId={activeId}
	onSelect={(c) => setActiveId(c.id)}
	onRename={renameConversation}
	onDelete={deleteConversation}
	onTogglePin={togglePin}
/>;

| Prop | Type | Default | Description | | ----------------------------------- | ------------------------- | ------------------------- | ---------------------------------------------- | | conversations | ConversationListEntry[] | — | Required. { id; title; timestamp; pinned? }. | | onSelect | (entry) => void | — | Required. | | activeId | string | — | | | onRename/onDelete/onTogglePin | (id: string) => void | — | Each adds its menu action only when given. | | emptyMessage | ReactNode | 'No conversations yet.' | |

ModelPicker

A pill trigger + dropdown radio list for picking exactly one model, built on @vinyasa/overlay's DropdownMenu.RadioGroup/.RadioItem.

import { ModelPicker } from '@vinyasa/ai-native/model-picker';

<ModelPicker
	options={[
		{ id: 'sonnet', label: 'Claude Sonnet', description: 'Balanced' },
		{ id: 'opus', label: 'Claude Opus', description: 'Most capable' },
	]}
	value={model}
	onValueChange={setModel}
/>;

| Prop | Type | Default | Description | | --------------- | ---------------------- | ------------------ | ---------------------------------------- | | options | ModelPickerOption[] | — | Required. { id; label; description? }. | | value | string | — | Required. | | onValueChange | (id: string) => void | — | Required. | | placeholder | string | 'Select a model' | Shown when value matches nothing. |

TokenUsageMeter

A context-window usage bar, built on @vinyasa/feedback's ProgressBar, escalating color as usage approaches or exceeds the max (warning at ≥90%, error at ≥100%).

import { TokenUsageMeter } from '@vinyasa/ai-native/token-usage-meter';

<TokenUsageMeter used={24500} max={32000} />;

| Prop | Type | Default | Description | | ------------- | --------------------------------------- | ------------------------- | ----------- | | used/max | number | — | Required. | | formatLabel | (used: number, max: number) => string | e.g. "12.4k/32k tokens" | |

AttachmentPreview

A full-size look at one attachment in a Dialog (from @vinyasa/overlay) — an image preview when previewUrl is given, otherwise a generic file-icon fallback. Standalone/caller-triggered, with no trigger prop — same as Dialog itself.

import { AttachmentPreview } from '@vinyasa/ai-native/attachment-preview';

<AttachmentPreview
	attachment={selectedAttachment}
	open={previewOpen}
	onOpenChange={setPreviewOpen}
/>;

| Prop | Type | Default | Description | | ------------ | ------------------------ | ------------ | ------------------------------------------------------- | | attachment | PromptEditorAttachment | — | Required. Reuses PromptEditor's own attachment shape. | | open | boolean | uncontrolled | |

CitationPopover

A numbered (or custom) citation marker that opens a Popover (from @vinyasa/overlay) showing the cited source's title, URL, and snippet.

import { CitationPopover } from '@vinyasa/ai-native/citation-popover';

<CitationPopover
	index={1}
	title="React docs — useEffect"
	url="https://react.dev/reference/react/useEffect"
	snippet="..."
/>;

| Prop | Type | Default | Description | | --------------- | -------------- | ------- | --------------------------------------------------- | | index | number | — | Renders the default [n] marker. | | trigger | ReactElement | — | Custom marker; wins over index if both are given. | | title | ReactNode | — | Required. | | url/snippet | string | — | |

CitationsPanel

The "show every source at once" counterpart to CitationPopover's one-at-a-time marker — a plain content list with no panel chrome of its own, meant to compose into ContextDrawer (see below).

import { CitationsPanel } from '@vinyasa/ai-native/citations-panel';

<CitationsPanel sources={sources} />;

| Prop | Type | Default | Description | | -------------- | ------------------------ | --------------------------------- | -------------------------------------------------- | | sources | CitationsPanelSource[] | — | Required. { id; index?; title; url?; snippet? }. | | emptyMessage | ReactNode | 'No sources for this response.' | |

ContextDrawer

A fixed-width side panel for showing retrieved context or documents fed to the model. Renders nothing when closed.

import { CitationsPanel } from '@vinyasa/ai-native/citations-panel';
import { ContextDrawer } from '@vinyasa/ai-native/context-drawer';

<ContextDrawer title="Sources" open={open} onOpenChange={setOpen}>
	<CitationsPanel sources={sources} />
</ContextDrawer>;

| Prop | Type | Default | Description | | ------------------ | ----------- | ----------------- | ----------- | | children | ReactNode | — | Required. | | title | ReactNode | — | | | open | boolean | uncontrolled | | | width | string | '20rem' | | | closeButtonLabel | string | 'Close context' | |

SemanticHighlight

Inline highlighting of AI-annotated spans (entities, citations, edits) within plain text, each span carrying its own semantic category — distinct from @vinyasa/typography's plain Highlight/Mark.

import { SemanticHighlight } from '@vinyasa/ai-native/semantic-highlight';

<SemanticHighlight
	text="Contact Jamie Reyes about the Q3 report."
	ranges={[
		{ start: 8, end: 19, category: 'info', label: 'Entity: person' },
		{ start: 27, end: 36, category: 'success', label: '92% confidence' },
	]}
/>;

| Prop | Type | Default | Description | | -------- | -------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | text | string | — | Required. | | ranges | SemanticHighlightRange[] | — | Required. { start; end; category?: 'neutral' \| 'info' \| 'success' \| 'warning' \| 'error'; label? }. Offsets into text, not substring search. |

buildSemanticHighlightSegments is also exported standalone as a pure helper for computing the rendered segments yourself.

AgentTimeline

A vertical, append-only log (role="log") of heterogeneous agent events — tool calls, file edits, thoughts — each with a status marker.

import { AgentTimeline } from '@vinyasa/ai-native/agent-timeline';

<AgentTimeline
	events={[
		{
			id: '1',
			type: 'tool_call',
			label: 'Reading src/index.ts',
			timestamp: new Date(),
			status: 'success',
		},
		{
			id: '2',
			type: 'file_edit',
			label: 'Editing README.md',
			timestamp: new Date(),
			status: 'running',
		},
	]}
/>;

| Prop | Type | Default | Description | | -------------- | ---------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | events | AgentTimelineEvent[] | — | Required. { id; type: string; label: ReactNode; timestamp: Date; status: 'pending' \| 'running' \| 'success' \| 'error' }. | | emptyMessage | ReactNode | 'No activity yet.' | |

ExecutionConsole

A monospace, terminal-style output panel that auto-scrolls to the newest line — for showing command or tool output.

import { ExecutionConsole } from '@vinyasa/ai-native/execution-console';

<ExecutionConsole
	lines={[
		{ id: '1', text: '$ pnpm test', tone: 'muted' },
		{ id: '2', text: '✓ 42 passed', tone: 'success' },
	]}
/>;

| Prop | Type | Default | Description | | -------------- | ------------------------ | ------------------ | ------------------------------------------------------------------------------------------- | | lines | ExecutionConsoleLine[] | — | Required. { id; text; tone?: 'default' \| 'muted' \| 'success' \| 'warning' \| 'error' }. | | emptyMessage | ReactNode | 'No output yet.' | | | height | string | '16rem' | Max height before scroll. |

CommandRunner

Composes ExecutionConsole with a run/stop/retry control row and a status dot, for a single runnable command's output. Owns no execution logic itself — the caller drives status and lines.

import { CommandRunner } from '@vinyasa/ai-native/command-runner';

<CommandRunner title="pnpm test" status={status} lines={lines} onRun={runTests} onStop={cancel} />;

| Prop | Type | Default | Description | | ----------------------------------- | --------------------------------------------- | -------------------------- | ---------------------------------------- | | lines | ExecutionConsoleLine[] | — | Required. | | status | 'idle' \| 'running' \| 'success' \| 'error' | — | Required. | | onRun | () => void | — | Required. | | onStop | () => void | — | Only used while status is 'running'. | | title | ReactNode | — | | | runLabel/retryLabel/stopLabel | string | 'Run'/'Retry'/'Stop' | |

AIDiffViewer

Renders a caller-supplied, pre-computed diff (no diffing engine, no syntax highlighting) with per-hunk accept/reject controls.

import { AIDiffViewer } from '@vinyasa/ai-native/ai-diff-viewer';

<AIDiffViewer
	hunks={[
		{
			id: 'h1',
			header: 'src/app.ts',
			lines: [
				{ type: 'remove', text: 'old()' },
				{ type: 'add', text: 'new()' },
			],
		},
	]}
	onAcceptHunk={(h) => accept(h.id)}
	onRejectHunk={(h) => reject(h.id)}
/>;

| Prop | Type | Default | Description | | --------------- | --------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------- | | hunks | DiffHunk[] | — | Required. { id; header?: ReactNode; lines: DiffLine[] }, DiffLine is { type: 'add' \| 'remove' \| 'context'; text }. | | onAcceptHunk | (hunk: DiffHunk) => void | — | Omitting either this or onRejectHunk hides that control. | | onRejectHunk | (hunk: DiffHunk) => void | — | | | hunkDecisions | Partial<Record<string, 'accepted' \| 'rejected'>> | — | Caller-tracked decision state — a decided hunk shows its settled state instead of controls. |

WorkflowBuilder

A themed wrapper around @xyflow/react's ReactFlow for visualizing or editing an agent's workflow graph, with custom node/edge components and a built-in status dot per node. ReactFlowProvider and its base stylesheet are wrapped/imported internally — no extra setup needed.

import { WorkflowBuilder } from '@vinyasa/ai-native/workflow-builder';

<WorkflowBuilder
	nodes={[
		{ id: '1', position: { x: 0, y: 0 }, data: { label: 'Fetch data', status: 'success' } },
		{ id: '2', position: { x: 200, y: 0 }, data: { label: 'Summarize', status: 'running' } },
	]}
	edges={[{ id: 'e1-2', source: '1', target: '2', data: { active: true } }]}
	onNodesChange={onNodesChange}
	onEdgesChange={onEdgesChange}
/>;

| Prop | Type | Default | Description | | ------------------------------- | ------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | nodes | WorkflowBuilderNode[] | — | Required. An @xyflow/react Node<WorkflowNodeData>; data is { label: ReactNode; status?: 'idle' \| 'running' \| 'success' \| 'error' }. | | edges | WorkflowBuilderEdge[] | — | Required. Edge<WorkflowEdgeData>; data is { active?: boolean } to highlight the current path. | | onNodesChange/onEdgesChange | ReactFlow's own change handlers | — | Mirrors ReactFlow's controlled API. | | onConnect | ReactFlow's own connect handler | — | Omit to disable interactive connection-dragging. | | fitView | boolean | true | | | height | string | '24rem' | |

This component owns no graph logic itself — it's a pure controlled wrapper; your app supplies nodes/edges and reacts to the change handlers.

AssistantPanel

The full composite "drop-in assistant widget" — bundles ChatThread and PromptEditor (plus StreamingResponse for an in-flight message) into one panel with a header, scroll management, and older-message pagination. Auto-scrolls to the bottom on a new message only if the user was already near the bottom, and preserves scroll position when older messages are prepended.

import { AssistantPanel } from '@vinyasa/ai-native/assistant-panel';

<AssistantPanel
	title="Assistant"
	messages={messages}
	onSendMessage={sendMessage}
	onClose={() => setOpen(false)}
	isSubmitting={isWaiting}
	hasMoreOlderMessages={hasMore}
	onLoadOlderMessages={loadMore}
/>;

| Prop | Type | Default | Description | | --------------------------------------------------------------------- | ------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | messages | AssistantPanelMessage[] | — | Required. { id; role: 'user' \| 'assistant'; content: string; streaming?: boolean } — streaming renders via StreamingResponse. | | onSendMessage | (text: string) => void | — | Required. | | title | ReactNode | — | | | onClose | () => void | — | | | isSubmitting | boolean | false | | | emptyMessage | ReactNode | 'Ask a question to get started.' | Pass an <AssistantEmptyState> for the full empty-state pattern. | | hasMoreOlderMessages/isLoadingOlderMessages/onLoadOlderMessages | mixed | — | Infinite-scroll-up pagination. | | attachments/onRemoveAttachment, leading/trailing | mixed | — | Passed straight through to the internal PromptEditor. |

Subpath imports

Every component is imported by its own subpath, named after its directory: action-suggestions, agent-timeline, ai-diff-viewer, assistant-empty-state, assistant-panel, attachment-preview, chat-thread, citation-popover, citations-panel, command-runner, context-drawer, conversation-list, execution-console, message-actions, message-version-pager, model-picker, prompt-editor, prompt-history, reasoning-inspector, semantic-highlight, streaming-response, thinking-indicator, token-usage-meter, tool-call-card, workflow-builder. There is no root @vinyasa/ai-native entry, so import { X } from '@vinyasa/ai-native' fails to resolve. See "This package has no root export" above for why.

Development

From the repository root:

pnpm --filter @vinyasa/ai-native build
pnpm --filter @vinyasa/ai-native test
pnpm --filter @vinyasa/ai-native lint
pnpm storybook   # AI Native/<ComponentName>