Skip to content

AgentInterface AssistantMessage override cannot render tool activity after #916 #1128

Description

@LemonPhase

Summary: components.AssistantMessage could render live tool progress before 0.13; after #916 it isn't mounted during the tool-only phase and receives toolCalls: []. There's no supported way to replace the turn-level timeline.

Question for maintainers: would you prefer restoring the messageGroup prop #916's description mentions, or adding a ToolCallTimeline slot to AgentInterfaceComponents? Happy to implement either with tests and docs once you confirm.

Describe the bug

Before @openuidev/react-ui 0.13, an AgentInterface components.AssistantMessage override represented the complete assistant turn. It could use useToolActivities() to render live tool progress as well as the final answer.

After PR #916, assistant/tool messages are grouped by the internal InterleavedTurn. That component now:

  1. Renders ToolCallTimeline directly, with no component override.
  2. Does not mount the custom AssistantMessage during the tool-only phase.
  3. Passes only the final answer to the custom component.
  4. Explicitly removes its tool calls:
const answerMessage = answer ? {...answer, toolCalls: []} : null;

As a result, an application cannot replace the built-in turn-level tool timeline while continuing to use AgentInterface.

This appears to be a regression in the documented customization contract. The current message-rendering documentation describes components as the escape hatch from built-in rendering and says custom components render every assistant and user message:

https://github.com/thesysdev/openui/blob/main/docs/content/docs/agent/customize/message-rendering.mdx

PR #916 also states:

New optional messageGroup prop on AssistantMessageComponent — existing custom components are unaffected

However, messageGroup is absent from the merged implementation, 0.13.10, and current main.

To Reproduce

  1. Configure AgentInterface with an LLM adapter that produces a tool call, tool result, and final assistant answer.

  2. Supply a custom assistant component:

    import {
      AgentInterface,
      type AssistantMessage,
      useThread,
      useToolActivities,
    } from "@openuidev/react-ui";
    
    function CustomAssistantMessage({
      message,
      isStreaming,
    }: {
      message: AssistantMessage;
      isStreaming: boolean;
    }) {
      const messages = useThread((state) => state.messages);
      const activities = useToolActivities(message, messages);
    
      return (
        <div>
          <div>Activities visible to override: {activities.length}</div>
          <div>{message.content}</div>
        </div>
      );
    }
    
    <AgentInterface
      llm={llm}
      components={{AssistantMessage: CustomAssistantMessage}}
    />;
  3. Start a turn that calls one or more tools.

  4. Observe the UI while the tools are running and after the answer begins.

Actual behavior

  • During the tool-only phase, only the built-in ToolCallTimeline is rendered.
  • CustomAssistantMessage cannot render live tool progress because it is not mounted for that phase.
  • When it is mounted for the final answer, message.toolCalls has been replaced with an empty array, so useToolActivities(message, messages) returns no activities for the turn.
  • There is no AgentInterface prop or component slot that suppresses and replaces the built-in timeline.

This prevents applications from implementing a custom presentation such as a collapsed-by-default:
Show research steps · used 3 tools
panel that updates live without the built-in staggered timeline.

Expected behavior

There should be a supported way to replace the turn-level tool-call timeline without rebuilding the entire thread or chat shell. The default behavior should remain exactly as it is when no override is provided.

Suggested solution

Add an optional timeline component to AgentInterfaceComponents, for example:

export type ToolCallTimelineComponent = React.ComponentType<{
  activities: ToolActivity[];
  steps: TimelineStep[];
  isLast: boolean;
  awaitingResponse: boolean;
}>;

export interface AgentInterfaceComponents {
  AssistantMessage?: AssistantMessageComponent;
  UserMessage?: UserMessageComponent;
  ToolCallTimeline?: ToolCallTimelineComponent;
}

InterleavedTurn could use the override when provided and otherwise preserve the existing implementation:

{turnActivities.length > 0 &&
  (CustomToolCallTimeline ? (
    <CustomToolCallTimeline
      activities={turnActivities}
      steps={steps}
      isLast={turnLive}
      awaitingResponse={turnLive && !answerStarted}
    />
  ) : (
    <ToolCallTimeline
      activities={turnActivities}
      steps={steps}
      isLast={turnLive}
      forceDefault
      awaitingResponse={turnLive && !answerStarted}
    />
  ))}

It would also be useful to expose the equivalent prop on AgentInterface.Messages, mirroring its existing assistantMessage and userMessage props.

The exact API naming is open for discussion. The required capability is for the replacement component to:

  • Mount as soon as the first tool activity appears, before answer content.
  • Receive live activities and the ordered timeline steps.
  • Replace, rather than render alongside, the built-in timeline.
  • Leave matched artifact/tool previews unchanged.
  • Preserve all existing behavior when the override is omitted.

ToolActivity, TimelineStep, ToolCallTimeline, TimelineEntry, and useToolActivities are already public, so this should require little new public surface.

Alternatives considered

  1. Continue using components.AssistantMessage

    This cannot render during the tool-only phase, and the message it eventually receives has toolCalls removed.

  2. Use artifactRenderers

    These customize matched tool previews, not the turn-level timeline or its wrapper. The built-in timeline remains present.

  3. Hide the built-in timeline with CSS

    It can be hidden, but there is still no custom component mounted during the tool-only phase to replace it.

  4. Build a custom thread with AgentInterface.Route

    Technically possible, but Route is intended for alternate pages. The application would need to own message grouping, turn rendering, loading and error behavior, routing details, and future compatibility.

  5. Build a complete shell using react-headless

    Provides full control but requires replacing substantially more of AgentInterface.

  6. Patch the installed package

    A temporary workaround, but it must be maintained across upgrades and does not provide a supported public API.

Environment

  • Worked as expected: @openuidev/react-ui@0.12.1
  • Regression observed after: @openuidev/react-ui@0.13.x
  • Confirmed absent from: @openuidev/react-ui@0.13.10
  • React: 18.3.1
  • Browser/OS: not browser-specific

Additional context

The original customization renders one collapsed research-steps panel per assistant turn. It updates its tool count during streaming and expands to show the existing public TimelineEntry components.

I would be happy to open a focused PR with the component slot, documentation, and a regression test after a maintainer confirms the preferred API shape.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions