Skip to content

fix(shell): render text-only A2A fallbacks#836

Open
stablegenius49 wants to merge 1 commit intogoogle:mainfrom
stablegenius49:pr-factory/issue-585-text-fallback
Open

fix(shell): render text-only A2A fallbacks#836
stablegenius49 wants to merge 1 commit intogoogle:mainfrom
stablegenius49:pr-factory/issue-585-text-fallback

Conversation

@stablegenius49
Copy link

Summary

  • convert text-only A2A task responses into a simple A2UI Text surface in the Lit shell client
  • preserve normal A2UI data-part responses unchanged
  • add regression tests for data-part passthrough and single/multi-text fallbacks

Fixes #585.

Why

The Lit shell client currently drops text parts and only forwards data parts into the A2UI processor. When an agent returns a text-only fallback message, the UI ends up blank even though the backend sent a useful error/fallback string.

Testing

  • npm install in renderers/web_core
  • npm run build:tsc in renderers/web_core
  • npm install in renderers/markdown/markdown-it
  • npm run build in renderers/markdown/markdown-it
  • npm install in samples/client/lit/shell
  • npm run build:tsc in samples/client/lit/shell
  • node --test --enable-source-maps dist/tests/client.test.js

@google-cla
Copy link

google-cla bot commented Mar 12, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a helpful fallback mechanism to render text-only A2A responses, preventing a blank UI. The new convertA2APartsToMessages function correctly transforms text parts into A2UI components, and the logic is well-supported by new regression tests. I've provided one suggestion to refactor the new function to improve its maintainability by reducing some code duplication.

Comment on lines +50 to +113
if (textMessages.length === 1) {
const rootId = textFallbackComponentId(0);
return [
{
beginRendering: {
root: rootId,
surfaceId: TEXT_FALLBACK_SURFACE_ID,
},
},
{
surfaceUpdate: {
surfaceId: TEXT_FALLBACK_SURFACE_ID,
components: [
{
id: rootId,
component: {
Text: {
usageHint: "body" as const,
text: { literalString: textMessages[0] },
},
},
},
],
},
},
];
}

const childIds = textMessages.map((_, index) => textFallbackComponentId(index));

return [
{
beginRendering: {
root: TEXT_FALLBACK_ROOT_ID,
surfaceId: TEXT_FALLBACK_SURFACE_ID,
},
},
{
surfaceUpdate: {
surfaceId: TEXT_FALLBACK_SURFACE_ID,
components: [
{
id: TEXT_FALLBACK_ROOT_ID,
component: {
Column: {
children: {
explicitList: childIds,
},
},
},
},
...textMessages.map((text, index) => ({
id: childIds[index],
component: {
Text: {
usageHint: "body" as const,
text: { literalString: text },
},
},
})),
],
},
},
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for handling single and multiple text messages is a bit repetitive. Both branches of the if statement construct and return a similar array of beginRendering and surfaceUpdate messages. You can reduce this duplication by first determining the rootId and components array based on the number of text messages, and then constructing the final message array once.

  let rootId: string;
  let components: v0_8.Types.ComponentInstance[];

  if (textMessages.length === 1) {
    rootId = textFallbackComponentId(0);
    components = [
      {
        id: rootId,
        component: {
          Text: {
            usageHint: "body" as const,
            text: { literalString: textMessages[0] },
          },
        },
      },
    ];
  } else {
    rootId = TEXT_FALLBACK_ROOT_ID;
    const childIds = textMessages.map((_, index) =>
      textFallbackComponentId(index)
    );
    components = [
      {
        id: TEXT_FALLBACK_ROOT_ID,
        component: {
          Column: {
            children: {
              explicitList: childIds,
            },
          },
        },
      },
      ...textMessages.map((text, index) => ({
        id: childIds[index],
        component: {
          Text: {
            usageHint: "body" as const,
            text: { literalString: text },
          },
        },
      })),
    ];
  }

  return [
    {
      beginRendering: {
        root: rootId,
        surfaceId: TEXT_FALLBACK_SURFACE_ID,
      },
    },
    {
      surfaceUpdate: {
        surfaceId: TEXT_FALLBACK_SURFACE_ID,
        components,
      },
    },
  ];

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

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

Text fallback from agent not rendered in A2UI, page refreshes silently

1 participant