logoassistant-ui

Message Part Grouping

Overview

This feature is experimental and the API may change in future versions.

The Parent ID Grouping feature allows you to group related message parts together by assigning them a common parentId. This is useful for organizing content hierarchically, such as grouping research sources with their findings, or organizing multi-step tool executions.

Use it in your application

Use the MessagePrimitive.Unstable_PartsGroupedByParentId component instead of the regular MessagePrimitive.Parts component:

/components/assistant-ui/thread.tsx
const : <
  <{ : string | undefined; : number[] }>
> = ({ , ,  }) => {
  if (!) {
    // Ungrouped parts - render directly
    return <>{}</>;
  }

  return (
    < ="bg-muted/20 my-2 rounded-lg border p-4">
      < ="mb-2 text-sm font-medium">
        Group: {} ({.} parts)
      </>
      < ="space-y-2">{}</>
    </>
  );
};

const :  = () => {
  return (
    <. ="...">
      < ="...">
        <.
          ={{ :  }}
        />
      </>
      < />

      < ="..." />
    </.>
  );
};

How it works

  1. Message parts with parent IDs: Add a parentId field to your message parts:

    {
      type: "text",
      text: "Research finding about climate change",
      parentId: "climate-research"
    }
  2. Automatic grouping: Parts with the same parentId are automatically grouped together

  3. Ordering: Groups appear in the order of the first occurrence of each parent ID

  4. Ungrouped parts: Parts without a parentId appear after all grouped parts

Setting Parent IDs

In Python (assistant-stream)

Use the with_parent_id() method on the RunController:

from assistant_stream import create_run

async def my_run(controller):
    # Regular message part
    controller.append_text("Starting research...")

    # Grouped parts with parent ID
    research_controller = controller.with_parent_id("research-123")

    await research_controller.add_tool_call("search", {"query": "climate data"})
    research_controller.append_source({
        "id": "source-1",
        "url": "https://example.com/climate-data",
        "title": "Climate Data Report"
    })
    research_controller.append_text("Key findings from the research:")
    research_controller.append_text("• Global temperatures rising")
    research_controller.append_text("• Sea levels increasing")

    # Back to ungrouped content
    controller.append_text("In conclusion...")

In TypeScript (assistant-stream)

Use the withParentId() method on the AssistantStreamController:

import { createAssistantStream } from "@assistant-ui/react/assistant-stream";

const stream = createAssistantStream(async (controller) => {
  // Regular message part
  controller.appendText("Starting research...");

  // Grouped parts with parent ID
  const researchController = controller.withParentId("research-123");

  await researchController.addToolCallPart({
    toolName: "search",
    args: { query: "climate data" },
  });
  researchController.appendSource({
    type: "source",
    id: "source-1",
    url: "https://example.com/climate-data",
    title: "Climate Data Report",
  });
  researchController.appendText("Key findings from the research:");
  researchController.appendText("• Global temperatures rising");
  researchController.appendText("• Sea levels increasing");

  // Back to ungrouped content
  controller.appendText("In conclusion...");
});

With External Store

When using the external store runtime, include the parentId in your message parts:

const messages = [
  {
    role: "assistant",
    content: [
      {
        type: "text",
        text: "Let me search for information...",
      },
      {
        type: "tool-call",
        toolCallId: "call-1",
        toolName: "search",
        args: { query: "climate change" },
        parentId: "search-results",
      },
      {
        type: "source",
        sourceType: "url",
        id: "source-1",
        url: "https://example.com",
        title: "Climate Report",
        parentId: "search-results",
      },
      {
        type: "text",
        text: "Based on the search results:",
        parentId: "search-results",
      },
    ],
  },
];

Props

The Group component receives the following props:

  • parentId: The parent ID shared by all parts in this group (undefined for ungrouped parts)
  • indices: Array of indices for the parts in this group
  • children: The rendered message part components

Examples

Collapsible Research Groups

import { useState } from "react";
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";

const CollapsibleGroup: FC<
  PropsWithChildren<{ parentId: string | undefined; indices: number[] }>
> = ({ parentId, indices, children }) => {
  const [isCollapsed, setIsCollapsed] = useState(false);

  if (!parentId) return <>{children}</>;

  return (
    <div className="my-2 overflow-hidden rounded-lg border">
      <button
        onClick={() => setIsCollapsed(!isCollapsed)}
        className="hover:bg-muted/50 flex w-full items-center justify-between p-3"
      >
        <span>Research Group ({indices.length} items)</span>
        {isCollapsed ? <ChevronDownIcon /> : <ChevronUpIcon />}
      </button>
      {!isCollapsed && <div className="border-t p-3">{children}</div>}
    </div>
  );
};

Labeled Groups with Icons

const LabeledGroup: FC<
  PropsWithChildren<{ parentId: string | undefined; indices: number[] }>
> = ({ parentId, indices, children }) => {
  if (!parentId) return <>{children}</>;

  const getGroupLabel = (id: string) => {
    if (id.includes("research")) return "🔍 Research";
    if (id.includes("analysis")) return "📊 Analysis";
    if (id.includes("summary")) return "📝 Summary";
    return "📁 " + id;
  };

  return (
    <div className="bg-muted/30 my-3 rounded-lg p-4">
      <h4 className="mb-2 text-sm font-semibold">{getGroupLabel(parentId)}</h4>
      <div className="space-y-2">{children}</div>
    </div>
  );
};

Use Cases

Parent ID grouping is particularly useful for:

  • Research assistants: Group sources, findings, and analysis together
  • Multi-step processes: Organize related tool calls and their results
  • Hierarchical content: Create nested or categorized information structures
  • Context preservation: Keep related information visually connected