Skip to content

Commit df19307

Browse files
committed
fixup! feat(ui): add A2A error extension (work in progress)
Signed-off-by: Petr Bulánek <bulanek.petr@gmail.com>
1 parent 830c2eb commit df19307

10 files changed

Lines changed: 139 additions & 56 deletions

File tree

apps/agentstack-ui/src/api/utils.ts

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { Toast } from '#contexts/Toast/toast-context.ts';
1313
import type { Agent } from '#modules/agents/api/types.ts';
1414
import { NEXTAUTH_URL, TRUST_PROXY_HEADERS } from '#utils/constants.ts';
1515
import { isNotNull } from '#utils/helpers.ts';
16-
import { createSection, joinSections } from '#utils/markdown.ts';
16+
import { createMarkdownCodeBlock, createMarkdownSection, joinMarkdownSections } from '#utils/markdown.ts';
1717

1818
import { A2AExtensionError, ApiError, ApiValidationError, HttpError, UnauthenticatedError } from './errors';
1919
import type { A2AErrorMetadata, ApiErrorCode, ApiErrorResponse, ApiValidationErrorResponse } from './types';
@@ -68,7 +68,19 @@ export async function handleStream<T>({
6868
}
6969
}
7070

71-
export function getErrorMessage(error: unknown) {
71+
export function getErrorTitle(error: unknown) {
72+
return typeof error === 'object' && isNotNull(error) && 'title' in error ? (error.title as string) : undefined;
73+
}
74+
75+
export function getErrorMessage(error: unknown, includeMessage = true) {
76+
if (!includeMessage) {
77+
return;
78+
}
79+
80+
if (error instanceof A2AExtensionError) {
81+
return createA2AErrorMessage(error);
82+
}
83+
7284
return typeof error === 'object' && isNotNull(error) && 'message' in error ? (error.message as string) : undefined;
7385
}
7486

@@ -116,43 +128,36 @@ export async function getProxyHeaders(headers: Headers, url?: URL) {
116128
}
117129

118130
export function buildErrorToast({ metadata = {}, error }: { metadata?: QueryMetadataError; error: unknown }): Toast {
119-
const { includeErrorMessage } = metadata;
131+
const { title = 'An error occurred', includeErrorMessage } = metadata;
120132

121-
const defaults: Partial<Toast> = {
133+
return {
122134
kind: 'error',
135+
title: getErrorTitle(error) ?? title,
136+
message: joinMarkdownSections([metadata.message, getErrorMessage(error, includeErrorMessage)]),
123137
renderMarkdown: true,
124138
};
125-
126-
if (error instanceof A2AExtensionError) {
127-
const message = includeErrorMessage ? createA2AErrorMessage(error) : undefined;
128-
129-
return {
130-
...defaults,
131-
title: error.title,
132-
message,
133-
};
134-
}
135-
136-
const { title = 'An error occurred' } = metadata;
137-
const message = joinSections([metadata.message, includeErrorMessage ? getErrorMessage(error) : undefined]);
138-
139-
return {
140-
...defaults,
141-
title,
142-
message,
143-
};
144139
}
145140

146-
function createA2AErrorMessage(error: A2AErrorMetadata) {
147-
const { context, stacktrace } = error;
141+
export function createA2AErrorMessage(error: A2AErrorMetadata) {
142+
const { message: errorMessage, context, stacktrace } = error;
148143

149-
const errorMessage = getErrorMessage(error);
150144
const contextMessage = context
151-
? createSection({ heading: 'Context', content: JSON.stringify(context, null, 2) })
145+
? createMarkdownSection({
146+
heading: 'Context',
147+
content: createMarkdownCodeBlock({
148+
language: 'json',
149+
snippet: JSON.stringify(context, null, 2),
150+
}),
151+
})
152+
: undefined;
153+
const stacktraceMessage = stacktrace
154+
? createMarkdownSection({
155+
heading: 'Stacktrace',
156+
content: stacktrace,
157+
})
152158
: undefined;
153-
const stacktraceMessage = stacktrace ? createSection({ heading: 'Stacktrace', content: stacktrace }) : undefined;
154159

155-
const message = joinSections([errorMessage, contextMessage, stacktraceMessage]);
160+
const message = joinMarkdownSections([errorMessage, contextMessage, stacktraceMessage]);
156161

157162
return message;
158163
}

apps/agentstack-ui/src/components/CopySnippet/CopySnippet.module.scss

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
min-block-size: rem(48px);
1919
border: none;
20-
border-radius: 0;
2120
}
2221

2322
&.blog {
@@ -36,6 +35,11 @@
3635
.block & {
3736
padding: unset;
3837
}
38+
39+
pre {
40+
border-start-start-radius: $border-radius;
41+
border-end-start-radius: $border-radius;
42+
}
3943
}
4044

4145
.button {
@@ -48,6 +52,8 @@
4852
.block & {
4953
border: none;
5054
margin: rem(8px) rem(8px) auto 0;
55+
position: sticky;
56+
inset-block-start: rem(8px);
5157
}
5258

5359
> div,

apps/agentstack-ui/src/components/ErrorMessage/ErrorMessage.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import { ActionableNotification, Button, InlineLoading } from '@carbon/react';
77
import type { ReactNode } from 'react';
88

9-
import { LineClampText } from '#components/LineClampText/LineClampText.tsx';
10-
import { MarkdownContent } from '#components/MarkdownContent/MarkdownContent.tsx';
9+
import { NotificationMarkdownContent } from '#components/NotificationMarkdownContent/NotificationMarkdownContent.tsx';
1110

1211
import classes from './ErrorMessage.module.scss';
1312

@@ -24,11 +23,7 @@ export function ErrorMessage({ title, message, onRetry, isRefetching }: Props) {
2423
<ActionableNotification title={title} kind="error" lowContrast hideCloseButton>
2524
{(message || onRetry) && (
2625
<div className={classes.body}>
27-
{message && (
28-
<LineClampText lines={4} useBlockElement>
29-
<MarkdownContent>{message}</MarkdownContent>
30-
</LineClampText>
31-
)}
26+
{message && <NotificationMarkdownContent>{message}</NotificationMarkdownContent>}
3227

3328
{onRetry && (
3429
<Button size="sm" onClick={() => onRetry()} disabled={isRefetching}>

apps/agentstack-ui/src/components/MarkdownContent/components/Code.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function Code({ variant, inline, forceExpand, className, children }: Prop
2929

3030
if (language) {
3131
return (
32-
<CopySnippet variant={variant}>
32+
<CopySnippet variant={variant} className={className}>
3333
<SyntaxHighlighter language={language} variant={variant}>
3434
{children}
3535
</SyntaxHighlighter>
@@ -42,7 +42,7 @@ export function Code({ variant, inline, forceExpand, className, children }: Prop
4242
}
4343

4444
return (
45-
<CodeSnippet forceExpand={forceExpand} canCopy={!forceExpand}>
45+
<CodeSnippet forceExpand={forceExpand} canCopy={!forceExpand} className={className}>
4646
{children}
4747
</CodeSnippet>
4848
);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
.root {
7+
overflow-wrap: anywhere;
8+
9+
.markdown {
10+
> * + * {
11+
margin-block-start: $spacing-03;
12+
}
13+
> * + h1,
14+
> * + h2,
15+
> * + h3,
16+
> * + h4,
17+
> * + h5,
18+
> * + h6 {
19+
margin-block-start: $spacing-05;
20+
}
21+
h1 {
22+
@include type-style(heading-03);
23+
font-weight: 600;
24+
}
25+
h2 {
26+
@include type-style(heading-02);
27+
}
28+
h3,
29+
h4,
30+
h5,
31+
h6 {
32+
@include type-style(heading-01);
33+
}
34+
h5,
35+
h6 {
36+
+ * {
37+
margin-block-start: 0;
38+
}
39+
}
40+
}
41+
}
42+
43+
.code {
44+
max-block-size: (2 * $spacing-05 + 5 * rem(16px)); // 5 lines + padding
45+
overflow-y: auto;
46+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import clsx from 'clsx';
7+
import { useMemo } from 'react';
8+
import type { Components } from 'react-markdown';
9+
10+
import { LineClampText } from '#components/LineClampText/LineClampText.tsx';
11+
import { Code } from '#components/MarkdownContent/components/Code.tsx';
12+
import { MarkdownContent } from '#components/MarkdownContent/MarkdownContent.tsx';
13+
14+
import classes from './NotificationMarkdownContent.module.scss';
15+
16+
interface Props {
17+
children: string;
18+
}
19+
20+
export function NotificationMarkdownContent({ children }: Props) {
21+
const components: Components = useMemo(
22+
() => ({
23+
code: ({ className, ...props }) => <Code {...props} className={clsx(className, classes.code)} />,
24+
}),
25+
[],
26+
);
27+
28+
return (
29+
<LineClampText className={classes.root} lines={4} useBlockElement>
30+
<MarkdownContent className={classes.markdown} components={components}>
31+
{children}
32+
</MarkdownContent>
33+
</LineClampText>
34+
);
35+
}

apps/agentstack-ui/src/components/Toast/Toast.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import type { ValueTransition } from 'framer-motion';
1111
import { AnimatePresence, motion } from 'framer-motion';
1212
import { useCallback, useEffect, useState } from 'react';
1313

14-
import { LineClampText } from '#components/LineClampText/LineClampText.tsx';
15-
import { MarkdownContent } from '#components/MarkdownContent/MarkdownContent.tsx';
14+
import { NotificationMarkdownContent } from '#components/NotificationMarkdownContent/NotificationMarkdownContent.tsx';
1615
import type { ToastWithKey } from '#contexts/Toast/toast-context.ts';
1716
import { FADE_DURATION, FADE_EASE_ENTRANCE, FADE_EASE_EXIT, fadeProps } from '#utils/fadeProps.ts';
1817

@@ -92,9 +91,7 @@ export function Toast({
9291

9392
{message &&
9493
(renderMarkdown ? (
95-
<LineClampText className={classes.message} lines={4} useBlockElement>
96-
<MarkdownContent>{message}</MarkdownContent>
97-
</LineClampText>
94+
<NotificationMarkdownContent>{message}</NotificationMarkdownContent>
9895
) : (
9996
<div className={classes.message}>{message}</div>
10097
))}

apps/agentstack-ui/src/modules/messages/components/MessageError.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { getErrorMessage } from '#api/utils.ts';
6+
import { getErrorMessage, getErrorTitle } from '#api/utils.ts';
77
import { ErrorMessage } from '#components/ErrorMessage/ErrorMessage.tsx';
88
import type { UIAgentMessage } from '#modules/messages/types.ts';
99

@@ -20,10 +20,10 @@ export function MessageError({ message }: Props) {
2020
return;
2121
}
2222

23-
return (
24-
<ErrorMessage
25-
title={isFailed ? 'Failed to generate an agent message.' : 'Message generation has been cancelled.'}
26-
message={getErrorMessage(error)}
27-
/>
28-
);
23+
const errorTitle = isFailed
24+
? (getErrorTitle(error) ?? 'Failed to generate an agent message.')
25+
: 'Message generation has been cancelled.';
26+
const errorMessage = getErrorMessage(error);
27+
28+
return <ErrorMessage title={errorTitle} message={errorMessage} />;
2929
}

apps/agentstack-ui/src/modules/runs/contexts/agent-run/AgentRunProvider.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { v4 as uuid } from 'uuid';
1212

1313
import type { AgentA2AClient, ChatRun } from '#api/a2a/types.ts';
1414
import { createTextPart } from '#api/a2a/utils.ts';
15-
import { A2AExtensionError } from '#api/errors.ts';
1615
import { getErrorCode } from '#api/utils.ts';
1716
import { useHandleError } from '#hooks/useHandleError.ts';
1817
import type { Agent } from '#modules/agents/api/types.ts';

apps/agentstack-ui/src/utils/markdown.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { isNotNull } from './helpers';
77

8-
export function createCodeBlock(language: string, snippet: string) {
8+
export function createMarkdownCodeBlock({ language, snippet }: { language: string; snippet: string }) {
99
return `\`\`\`${language}\n${snippet}\n\`\`\``;
1010
}
1111

@@ -17,14 +17,14 @@ export function toMarkdownCitation({ text, sources }: { text: string; sources: s
1717
return `[${text}](citation:${sources.join(',')})`;
1818
}
1919

20-
export function createSection({ heading, content }: { heading: string; content: string }) {
20+
export function createMarkdownSection({ heading, content }: { heading: string; content: string }) {
2121
return `
22-
**${heading}**
22+
### ${heading}
2323
2424
${content}
2525
`;
2626
}
2727

28-
export function joinSections(sections: (string | undefined)[]) {
28+
export function joinMarkdownSections(sections: (string | undefined)[]) {
2929
return sections.filter(isNotNull).join('\n\n');
3030
}

0 commit comments

Comments
 (0)