mcpField
P
primitives

primitives

github.com/assistant-ui開発296.3k 回インストール1 日前に更新
リポジトリ

primitives について

primitivesは、カスタムチャットUIを構築する開発者向けの、assistant-uiのスタイルなしで組み合わせ可能なReactコンポーネントです。スレッド、メッセージ、入力欄の動作を備え、asChildで独自要素に適用できます。

主な機能

  1. 01スレッドとメッセージの組み合わせ部品
  2. 02入力、送信、キャンセル、添付ファイル処理
  3. 03自動スクロールと実行状態の管理
  4. 04分岐切替、編集、メッセージ操作
  5. 05キーボードショートカットとフォーカス管理

活用シーン

  1. 01カスタムのチャットスレッド画面を構築する
  2. 02メッセージに編集、分岐、操作バーを加える
  3. 03添付ファイルや引用に対応する入力欄を作る
  4. 04実行状態に応じてUI表示を切り替える

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/assistant-ui/skills --skill primitives

インストール後、Claude Code で/primitivesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
action-bar.md
composer-input.md
composer.md
mentions.md
message.md
messages.md
part-grouping.md
suggestions.md
thread.md

詳細

assistant-ui Primitives

最新の API については、常に assistant-ui.com/llms.txt を参照してください。

Primitives は、Radix スタイルのパート構成に従った、組み合わせ可能でスタイルのないコンポーネントです。.Root がコンテキストを提供し、.PartName の子要素がそれを読み取り、すべてのパートは asChild を受け付けて、ラッパーをレンダリングする代わりに自身の動作を独自の要素にマージできます。スタイルやレイアウトに関する意見は一切含まれず、動作のみを提供します。すなわち、キーボードショートカット、自動スクロール、ストリーミング状態、フォーカス管理、無効化ロジックです。いずれかを使用する前に、ツリーを AssistantRuntimeProvider(またはネストされた AuiProvider)でラップしてください。

リファレンス

インポート

import {
  AuiIf,
  ThreadPrimitive,
  ComposerPrimitive,
  MessagePrimitive,
  MessagePartPrimitive,
  ActionBarPrimitive,
  ActionBarMorePrimitive,
  BranchPickerPrimitive,
  AttachmentPrimitive,
  ErrorPrimitive,
  AssistantModalPrimitive,
  ChainOfThoughtPrimitive,
  SelectionToolbarPrimitive,
  SuggestionPrimitive,
  QueueItemPrimitive,
  ThreadListPrimitive,
  ThreadListItemPrimitive,
  ThreadListItemMorePrimitive,
} from "@assistant-ui/react";

プリミティブパーツ

プリミティブパーツ
ThreadPrimitive.Root, .Viewport, .ViewportProvider, .ViewportFooter, .Messages, .MessageByIndex, .Unstable_MessageById, .ScrollToBottom, .Suggestions, .SuggestionByIndex, .Suggestion, .Empty (非推奨), .If (非推奨)
ComposerPrimitive.Root, .Input, .Send, .Cancel, .AddAttachment, .Attachments, .AttachmentByIndex, .AttachmentDropzone, .Dictate, .StopDictation, .DictationTranscript, .Quote, .QuoteText, .QuoteDismiss, .Queue, .Unstable_TriggerPopoverRoot, .Unstable_TriggerPopover (.Directive / .Action 付き), .Unstable_TriggerPopoverCategories, .Unstable_TriggerPopoverCategoryItem, .Unstable_TriggerPopoverItems, .Unstable_TriggerPopoverItem, .Unstable_TriggerPopoverBack, .If (非推奨)
MessagePrimitive.Root, .Parts (.Content は非推奨のエイリアス), .PartByIndex, .GroupedParts, .Unstable_PartsGrouped, .Unstable_PartsGroupedByParentId, .Attachments, .AttachmentByIndex, .Quote, .Error, .GenerativeUI, .If (非推奨)
MessagePartPrimitive.Text, .Image, .InProgress, .Messages
ActionBarPrimitive.Root, .Copy, .Reload, .Edit, .Speak, .StopSpeaking, .FeedbackPositive, .FeedbackNegative, .ExportMarkdown
ActionBarMorePrimitive.Root, .Trigger, .Content, .Item, .Separator
BranchPickerPrimitive.Root, .Previous, .Next, .Number, .Count
AttachmentPrimitive.Root, .Name, .Remove, .unstable_Thumb
ErrorPrimitive.Root, .Message
AssistantModalPrimitive.Root, .Trigger, .Content, .Anchor
ChainOfThoughtPrimitive.Root, .AccordionTrigger, .Parts (レガシー、MessagePrimitive.GroupedParts を推奨)
SelectionToolbarPrimitive.Root, .Quote
SuggestionPrimitive.Title, .Description, .Trigger
QueueItemPrimitive.Text, .Steer, .Remove
ThreadListPrimitive.Root, .New, .Items, .ItemByIndex, .LoadMore
ThreadListItemPrimitive.Root, .Trigger, .Title, .Archive, .Unarchive, .Delete
ThreadListItemMorePrimitive.Root, .Trigger, .Content, .Item, .Separator

ThreadListPrimitive、ThreadListItemPrimitive、ThreadListItemMorePrimitive はマルチスレッドのサイドバーを制御します。このスキルでは上記の表でのみ扱います。カスタム UI の全体像、CRUD 操作、リモートアダプターのチュートリアルについては thread-list を参照してください。

AuiIf による条件付きレンダリング

新しい条件にはすべて AuiIf を使用してください。固定のブール値プロパティの代わりに、アシスタントの状態全体 (thread、message、composer、part、attachment) に対するセレクターを受け取り、ThreadPrimitive、MessagePrimitive、ComposerPrimitive にまだ同梱されている非推奨の .If を置き換えます。

<AuiIf condition={(s) => s.thread.isEmpty}>
  <WelcomeScreen />
</AuiIf>

<AuiIf condition={(s) => s.thread.isRunning}>
  <ComposerPrimitive.Cancel>Stop</ComposerPrimitive.Cancel>
</AuiIf>

<AuiIf
  condition={(s) => s.message.role === "assistant" && s.message.status?.type === "complete"}
>
  <FollowUpCard />
</AuiIf>

<AuiIf condition={(s) => s.composer.dictation != null}>
  <ComposerPrimitive.StopDictation>Stop</ComposerPrimitive.StopDictation>
</AuiIf>

AuiIf.Condition は、JSX の外部で条件関数に型を付けるためにエクスポートされています。s.message と s.part はメッセージまたはパーツのコンテキスト内でのみ値が入ります。s.thread と s.composer は常に利用できます。

カスタムスレッドの例

プリミティブから構築された完全なスレッド: ウェルカムサジェスチョン、インライン編集コンポーザー付きのロールごとのメッセージ、アクションバー、ブランチピッカー、そして実行中に Send を Cancel に切り替えるコンポーザーフッターです。

function CustomThread() {
  return (
    <ThreadPrimitive.Root className="flex h-full flex-col">
      <ThreadPrimitive.Viewport className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
        <AuiIf condition={(s) => s.thread.isEmpty}>
          <div className="flex flex-1 flex-col items-center justify-center gap-3">
            <p>Ask me anything.</p>
            <ThreadPrimitive.Suggestions>
              {() => (
                <SuggestionPrimitive.Trigger send className="rounded-lg border px-3 py-2">
                  <SuggestionPrimitive.Title />
                </SuggestionPrimitive.Trigger>
              )}
            </ThreadPrimitive.Suggestions>
          </div>
        </AuiIf>

        <ThreadPrimitive.Messages>
          {({ message }) => {
            if (message.role === "user") {
              return message.composer.isEditing ? <EditComposer /> : <UserMessage />;
            }
            return <AssistantMessage />;
          }}
        </ThreadPrimitive.Messages>

        <ThreadPrimitive.ViewportFooter className="sticky bottom-0 pt-2">
          <ComposerPrimitive.Root className="flex items-end gap-2 rounded-2xl border bg-background p-2">
            <ComposerPrimitive.Input
              placeholder="Send a message..."
              rows={1}
              className="flex-1 resize-none bg-transparent px-2 py-1.5 focus:outline-none"
            />
            <AuiIf condition={(s) => !s.thread.isRunning}>
              <ComposerPrimitive.Send className="rounded-full bg-primary px-3 py-1.5 text-primary-foreground" />
            </AuiIf>
            <AuiIf condition={(s) => s.thread.isRunning}>
              <ComposerPrimitive.Cancel className="rounded-full border px-3 py-1.5" />
            </AuiIf>
          </ComposerPrimitive.Root>
        </ThreadPrimitive.ViewportFooter>
      </ThreadPrimitive.Viewport>
    </ThreadPrimitive.Root>
  );
}

function UserMessage() {
  return (
    <MessagePrimitive.Root className="flex flex-col items-end gap-1">
      <MessagePrimitive.Quote>
        {({ text }) => <blockquote className="border-l pl-2 text-sm italic">{text}</blockquote>}
      </MessagePrimitive.Quote>
      <div className="max-w-[80%] rounded-2xl bg-primary px-4 py-2 text-primary-foreground">
        <MessagePrimitive.Parts>
          {({ part }) => {
            if (part.type === "text") return <MessagePartPrimitive.Text />;
            if (part.type === "image") return <MessagePartPrimitive.Image className="max-w-full rounded-lg" />;
            return null;
          }}
        </MessagePrimitive.Parts>
      </div>
      <ActionBarPrimitive.Root hideWhenRunning autohide="not-last">
        <ActionBarPrimitive.Edit>Edit</ActionBarPrimitive.Edit>
      </ActionBarPrimitive.Root>
    </MessagePrimitive.Root>
  );
}

function EditComposer() {
  return (
    <MessagePrimitive.Root className="flex justify-end">
      <ComposerPrimitive.Root className="w-[80%] rounded-2xl border p-2">
        <ComposerPrimitive.Input className="w-full resize-none bg-transparent focus:outline-none" />
        <div className="flex justify-end gap-2 pt-1">
          <ComposerPrimitive.Cancel className="rounded-md px-2 py-1 text-sm">Cancel</ComposerPrimitive.Cancel>
          <ComposerPrimitive.Send className="rounded-md bg-primary px-2 py-1 text-sm text-primary-foreground">
            Save
          </ComposerPrimitive.Send>
        </div>
      </ComposerPrimitive.Root>
    </MessagePrimitive.Root>
  );
}

function AssistantMessage() {
  return (
    <MessagePrimitive.Root className="flex flex-col items-start gap-1">
      <div className="max-w-[80%] rounded-2xl bg-muted px-4 py-2">
        <MessagePrimitive.Parts>
          {({ part }) => {
            switch (part.type) {
              case "text":
                return <p className="whitespace-pre-wrap"><MessagePartPrimitive.Text /></p>;
              case "image":
                return <MessagePartPrimitive.Image className="max-w-full rounded-lg" />;
              case "file":
                return (
                  <a href={part.data ?? part.url} download={part.filename} className="text-sm underline">
                    {part.filename ?? part.mimeType}
                  </a>
                );
              case "reasoning":
                return (
                  <details className="text-sm text-muted-foreground">
                    <summary>Thinking</summary>
                    {part.text}
                  </details>
                );
              case "source":
                return part.sourceType === "url" ? (
                  <a href={part.url} className="text-sm underline">{part.title ?? part.url}</a>
                ) : (
                  <span className="text-sm">{part.title}</span>
                );
              case "tool-call":
                return part.toolUI ?? <div className="rounded-md border p-2 text-sm">{part.toolName}</div>;
              case "data":
                return part.dataRendererUI ?? null;
              case "generative-ui":
                return <MessagePrimitive.GenerativeUI />;
              default:
                return null; // registered tool and data UIs still render when you return null
            }
          }}
        </MessagePrimitive.Parts>
      </div>
      <MessagePrimitive.Error>
        <ErrorPrimitive.Root className="text-sm text-destructive">
          <ErrorPrimitive.Message />
        </ErrorPrimitive.Root>
      </MessagePrimitive.Error>
      <div className="flex items-center gap-2">
        <BranchPickerPrimitive.Root hideWhenSingleBranch className="flex items-center gap-1 text-xs">
          <BranchPickerPrimitive.Previous>←</BranchPickerPrimitive.Previous>
          <span><BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count /></span>
          <BranchPickerPrimitive.Next>→</BranchPickerPrimitive.Next>
        </BranchPickerPrimitive.Root>
        <ActionBarPrimitive.Root hideWhenRunning autohide="not-last">
          <ActionBarPrimitive.Copy>Copy</ActionBarPrimitive.Copy>
          <ActionBarPrimitive.Reload>Regenerate</ActionBarPrimitive.Reload>
        </ActionBarPrimitive.Root>
      </div>
    </MessagePrimitive.Root>
  );
}

上記のアシスタントメッセージの .Parts コールバックは、パイプラインが渡す可能性のある 8 種類すべてのパーツを処理します: 3 つのモダリティパーツ (text、image、file)、4 つのプロバイダーチャネルパーツ (reasoning、source、tool-call、generative-ui)、そしてオープンエンドな data パーツです。default ケースから null を返した場合でも、名前で登録されたツール UI や name で登録されたデータレンダラーが自動的に引き継ぐことができます。そのパーツを完全に非表示にしたい場合は、代わりに <></> を返してください。

ブランチピッカー

BranchPickerPrimitive は最も近い MessagePrimitive.Root からブランチの状態を読み取るため、その中でレンダリングする必要があります。Previous と Next は境界で自動的に無効化されます (また、ランタイムが switchBranchDuringRun をサポートしていない限り、実行中も無効化されます)。hideWhenSingleBranch は、メッセージに実際に代替ブランチができるまでピッカー全体を非表示にするため、ブランチが 1 つという一般的なケースでアクション行がガタつくのを防ぎます。

<BranchPickerPrimitive.Root hideWhenSingleBranch className="inline-flex items-center gap-1">
  <BranchPickerPrimitive.Previous>←</BranchPickerPrimitive.Previous>
  <span><BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count /></span>
  <BranchPickerPrimitive.Next>→</BranchPickerPrimitive.Next>
</BranchPickerPrimitive.Root>

新しいブランチは、ユーザーメッセージが編集されて再送信されたとき、または ActionBarPrimitive.Reload (もしくは aui.message.reload()) がアシスタントメッセージを再生成したときに現れます。aui.message.switchToBranch を使ったプログラムによるブランチナビゲーションについては、messages.md を参照してください。

よくある落とし穴

プリミティブが何もレンダリングしない、またはプロバイダーの外でフックが例外をスローする

  • すべてのプリミティブは最も近いランタイムコンテキストから読み取ります。ツリーを AssistantRuntimeProvider runtime={...} (useChatRuntime や useLocalRuntime などのランタイムフックから取得) でラップするか、分離されたスコープのために AuiProvider extends={aui} config={AuiConfig({...})} をネストしてください。
  • ActionBarPrimitive、BranchPickerPrimitive、ErrorPrimitive はさらに MessagePrimitive.Root の祖先が必要です。SelectionToolbarPrimitive は ThreadPrimitive.Root の祖先が必要ですが、ThreadPrimitive.Viewport の外側に配置する必要があります。

何もスタイルが適用されない

  • プリミティブはクラスのない素のネイティブ要素 (<div>、<button>、<textarea> など) をレンダリングします。自分で className を追加するか、asChild を渡して、すでに持っているスタイル付きコンポーネントに動作をマージしてください。

ThreadPrimitive.ViewportSlack はもう存在しない

  • これはパブリック API から削除されました。トップアンカーのターゲット登録は、Viewport が turnAnchor="top" を持つ場合に MessagePrimitive.Root の内部で自動的に行われるようになりました。fillClampThreshold / fillClampOffset のカスタマイズは、代わりに Viewport の topAnchorMessageClamp に置き換えてください。

MessagePrimitive.Content、.Empty、およびすべての .If はまだ動作するがレガシー

  • .Content は .Parts の非推奨エイリアスです。ThreadPrimitive.Empty とすべてのプリミティブの .If は AuiIf に取って代わられました。まだ移行していないコードでのみ残しておいてください。

groupPartByType({ "mcp-app": [...] }) が黙ってマッチしなくなる

  • "mcp-app" キーは 0.15 で削除されました。代わりに "standalone-tool-call" を使用してください。これは、登録された UI が display: "standalone" をオプトインしているすべてのツール呼び出しにもマッチするスーパーセットです。詳細は part-grouping.md を参照してください。

Unstable_ プレフィックス付きの props、フック、プリミティブ

  • コンポーザートリガーポップオーバーファミリー、unstable_useComposerInput、unstable_useComposerInputHistory、unstable_useThreadMessageIds は明示的に不安定であり、メジャーバージョンのアップなしに変更される可能性があります。これらを使って構築しても安全ですが、アップグレードする前に依存している正確な動作を固定してください。

関連スキル

  • elements -- これらのプリミティブから構築された、プロジェクトにコピーできるスタイル付きコンポーネント
  • setup -- create、init、add、およびその他の CLI スキャフォールド
  • runtime -- useAui、useAuiState、AuiConfig、およびすべてのプリミティブが読み取る状態
  • tools -- MessagePrimitive.Parts 内で part.toolUI に解決されるツールキットの render エントリ
  • thread-list -- ThreadListPrimitive と ThreadListItemPrimitive の詳細

コメント