-
Notifications
You must be signed in to change notification settings - Fork 416
chore(frontend): bump A2A, AI SDK v6, streamdown v2; re-sync AI Elements #2389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 21 commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
d41b1ee
chore(frontend): bump A2A SDK, MCP SDK, AI SDK v6, streamdown v2
malinskibeniamin f7a51d0
fix(frontend): adapt useContextUsage to ai v6 LanguageModelUsage shape
malinskibeniamin 09e9059
feat(frontend): adopt MCP approval states and dynamic tools in ai-ele…
malinskibeniamin 7de96be
feat(frontend): adopt ConversationDownload and messagesToMarkdown
malinskibeniamin a58da54
refactor(frontend): re-sync low-risk ai-elements with upstream
malinskibeniamin 853c6b3
chore(frontend): revert MCP SDK bump to keep it in separate PR
malinskibeniamin 87c8531
fix(frontend): guard Context components against divide-by-zero
malinskibeniamin 588f687
test(frontend): extend Tool approval-flow and output-rendering coverage
malinskibeniamin a0cb227
test(frontend): cover Shimmer motion-cache and Response streamdown v2
malinskibeniamin 40004d9
test(frontend): cover A2A adapter mapFinishReason and getResponseMeta…
malinskibeniamin 1da7d40
test(frontend): add showcase browser tests + screenshots for PR #2389
malinskibeniamin 61d4116
style(frontend): apply biome formatter to a2a adapter test
malinskibeniamin 7acf04c
refactor(frontend): drop stale @ts-ignore and dead activeTextIds in A…
malinskibeniamin 38b359f
refactor(frontend): remove no-op text-delta handler
malinskibeniamin 821e52a
fix(frontend): copy button title uses displayName not raw toolName
malinskibeniamin 8fbe849
docs(frontend): regenerate PR screenshots with real theme styling
malinskibeniamin c4bdcb2
refactor(frontend): extract pure a2aEventToV2StreamParts from doStream
malinskibeniamin 2f5cdb2
test(frontend): cover a2aEventToV2StreamParts per-state branches
malinskibeniamin 59b32eb
refactor(frontend): extract parseA2AError into its own util
malinskibeniamin 1e1ec24
test(frontend): table-driven coverage for parseA2AError
malinskibeniamin 34b9465
chore(frontend): host PR screenshots on orphan branch instead of comm…
malinskibeniamin dc6a13f
chore(frontend): follow master to MCP SDK 1.29.0
malinskibeniamin 09624c5
chore(repo): drop unneeded frontend/docs/pr-screenshots gitignore entry
malinskibeniamin 3dfe7e0
chore(frontend): bump copyright year to 2026 across PR-added files
malinskibeniamin fc0e4e9
fix(frontend): harden Context divide-by-zero guard against NaN, Infin…
malinskibeniamin 8b3498c
test(frontend): align ai-elements browser tests with ADP UI utility c…
malinskibeniamin 4b0ad30
docs(frontend): clarify why Image drops uint8Array and renders from b…
malinskibeniamin 3a84337
feat(frontend): reject unknown A2A stream events with UnsupportedFunc…
malinskibeniamin fcae41e
feat(frontend): extend parseA2AError with title and actionable hint f…
malinskibeniamin ed95e1c
style(frontend): satisfy biome formatter and top-level-regex rule on …
malinskibeniamin ec89a98
chore(frontend): remove committed mcp inspector screenshot baseline
malinskibeniamin 81f3e6f
fix(frontend): stop double-dispatching prompts in A2A doStream
malinskibeniamin 47069d9
fix(frontend): distinguish ToolOutput error container from success
malinskibeniamin 65573f9
fix(frontend): capture nested Data payloads in parseA2AError regex
malinskibeniamin a566c64
refactor(frontend): extract chooseA2ASourceStream for testable transp…
malinskibeniamin 9452184
style(frontend): satisfy biome rules in a2a tests (ci-lint fix)
malinskibeniamin 52d70db
test(frontend): wrap MCP streaming hook state-updates in act()
malinskibeniamin 7491134
style(frontend): reorder imports per biome autoformat
malinskibeniamin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
frontend/src/components/ai-elements/context.browser.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| /** | ||
| * Copyright 2025 Redpanda Data, Inc. | ||
|
malinskibeniamin marked this conversation as resolved.
Outdated
|
||
| * | ||
| * Use of this software is governed by the Business Source License | ||
| * included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md | ||
| * | ||
| * As of the Change Date specified in that file, in accordance with | ||
| * the Business Source License, use of this software will be governed | ||
| * by the Apache License, Version 2.0 | ||
| */ | ||
|
|
||
| import type { LanguageModelUsage } from 'ai'; | ||
| import { useRef, useState, useEffect, type ReactNode } from 'react'; | ||
| import { afterEach, describe, test } from 'vitest'; | ||
| import { page } from 'vitest/browser'; | ||
| import { cleanup, render } from 'vitest-browser-react'; | ||
|
|
||
| import { ScreenshotFrame } from '../../__tests__/browser-test-utils'; | ||
| import { | ||
| Context, | ||
| ContextCacheUsage, | ||
| ContextContent, | ||
| ContextContentBody, | ||
| ContextContentFooter, | ||
| ContextContentHeader, | ||
| ContextInputUsage, | ||
| ContextOutputUsage, | ||
| ContextReasoningUsage, | ||
| ContextTrigger, | ||
| } from './context'; | ||
|
|
||
| const SCREENSHOT_DIR = '../../../docs/pr-screenshots'; | ||
|
|
||
| afterEach(() => { | ||
| cleanup(); | ||
| }); | ||
|
|
||
| // Render the HoverCardContent portal inside the screenshot frame so the whole | ||
| // card (trigger + popup body) is captured by a single element screenshot. | ||
| const PortalWithinFrame = ({ | ||
| children, | ||
| open, | ||
| }: { | ||
| children: (container: Element | undefined) => ReactNode; | ||
| open: boolean; | ||
| }) => { | ||
| const ref = useRef<HTMLDivElement | null>(null); | ||
| const [container, setContainer] = useState<Element | undefined>(undefined); | ||
| useEffect(() => { | ||
| if (ref.current) { | ||
| setContainer(ref.current); | ||
| } | ||
| }, []); | ||
| return ( | ||
| <div ref={ref} style={{ position: 'relative', display: 'inline-block' }}> | ||
| {open ? children(container) : children(undefined)} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| const ContextPanel = ({ | ||
| usedTokens, | ||
| maxTokens, | ||
| usage, | ||
| modelId, | ||
| }: { | ||
| usedTokens: number; | ||
| maxTokens: number; | ||
| usage?: LanguageModelUsage; | ||
| modelId?: string; | ||
| }) => ( | ||
| <ScreenshotFrame width={520}> | ||
| <div style={{ display: 'flex', gap: 24, alignItems: 'flex-start' }}> | ||
| <PortalWithinFrame open={true}> | ||
| {(container) => ( | ||
| <Context | ||
| maxTokens={maxTokens} | ||
| modelId={modelId} | ||
| open={true} | ||
| usage={usage} | ||
| usedTokens={usedTokens} | ||
| > | ||
| <ContextTrigger /> | ||
| <ContextContent align="start" container={container} side="bottom" sideOffset={8}> | ||
| <ContextContentHeader /> | ||
| <ContextContentBody> | ||
| <ContextInputUsage /> | ||
| <ContextOutputUsage /> | ||
| <ContextReasoningUsage /> | ||
| <ContextCacheUsage /> | ||
| </ContextContentBody> | ||
| <ContextContentFooter /> | ||
| </ContextContent> | ||
| </Context> | ||
| )} | ||
| </PortalWithinFrame> | ||
| </div> | ||
| </ScreenshotFrame> | ||
| ); | ||
|
|
||
| const shot = (name: string) => | ||
| page.getByTestId('screenshot-frame').screenshot({ path: `${SCREENSHOT_DIR}/${name}.png` }); | ||
|
|
||
| describe('Context hover-card screenshots', () => { | ||
| test('zero tokens (guards hide sub-rows)', async () => { | ||
| render(<ContextPanel maxTokens={200_000} usedTokens={0} />); | ||
| await shot('context-zero-tokens'); | ||
| }); | ||
|
|
||
| test('populated usage with all sub-objects', async () => { | ||
| render( | ||
| <ContextPanel | ||
| maxTokens={200_000} | ||
| modelId="anthropic/claude-3-5-sonnet" | ||
| usage={{ | ||
| inputTokens: 18_420, | ||
| outputTokens: 2_316, | ||
| reasoningTokens: 412, | ||
| cachedInputTokens: 5_800, | ||
| totalTokens: 21_148, | ||
| inputTokenDetails: { | ||
| noCacheTokens: 12_620, | ||
| cacheReadTokens: 5_800, | ||
| cacheWriteTokens: 0, | ||
| }, | ||
| outputTokenDetails: { | ||
| textTokens: 1_904, | ||
| reasoningTokens: 412, | ||
| }, | ||
| }} | ||
| usedTokens={21_148} | ||
| /> | ||
| ); | ||
| await shot('context-populated'); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| /** | ||
| * Copyright 2025 Redpanda Data, Inc. | ||
| * | ||
| * Use of this software is governed by the Business Source License | ||
| * included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md | ||
| * | ||
| * As of the Change Date specified in that file, in accordance with | ||
| * the Business Source License, use of this software will be governed | ||
| * by the Apache License, Version 2.0 | ||
| */ | ||
|
|
||
| import { render, screen } from '@testing-library/react'; | ||
| import type { LanguageModelUsage } from 'ai'; | ||
| import { describe, expect, test } from 'vitest'; | ||
|
|
||
| import { | ||
| Context, | ||
| ContextContent, | ||
| ContextContentFooter, | ||
| ContextContentHeader, | ||
| ContextInputUsage, | ||
| ContextOutputUsage, | ||
| ContextTrigger, | ||
| } from './context'; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Zero-token / edge-case guards for ContextContentHeader | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| describe('ContextContentHeader', () => { | ||
| test('renders 0% and 0 / 0 when both used and max tokens are zero', () => { | ||
| // This is the degenerate case that shows up when a conversation hasn't | ||
| // emitted any usage events yet. It must not render NaN, Infinity, or | ||
| // throw a divide-by-zero. | ||
| render( | ||
| <Context maxTokens={0} usedTokens={0}> | ||
| <ContextTrigger /> | ||
| <ContextContent> | ||
| <ContextContentHeader /> | ||
| </ContextContent> | ||
| </Context> | ||
| ); | ||
|
|
||
| // The trigger's percentage label should be 0% (Intl formats NaN as "NaN%" | ||
| // which is a regression signal for a divide-by-zero). | ||
| const triggerPct = screen.getByRole('button'); | ||
| expect(triggerPct.textContent ?? '').not.toMatch(/NaN|Infinity/); | ||
| }); | ||
|
|
||
| test('renders a sane percentage when usage is partial', () => { | ||
| render( | ||
| <Context maxTokens={1000} usedTokens={250}> | ||
| <ContextTrigger /> | ||
| </Context> | ||
| ); | ||
| const trigger = screen.getByRole('button'); | ||
| expect(trigger.textContent ?? '').toContain('25%'); | ||
| }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // ContextInputUsage / ContextOutputUsage zero-token suppression | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| describe('ContextInputUsage / ContextOutputUsage zero-token guard', () => { | ||
| const zeroUsage: LanguageModelUsage = { | ||
| inputTokens: 0, | ||
| outputTokens: 0, | ||
| totalTokens: 0, | ||
| }; | ||
|
|
||
| test('ContextInputUsage renders nothing when inputTokens is 0', () => { | ||
| const { container } = render( | ||
| <Context maxTokens={100} usage={zeroUsage} usedTokens={0}> | ||
| <ContextContent> | ||
| <ContextInputUsage /> | ||
| </ContextContent> | ||
| </Context> | ||
| ); | ||
| // The component short-circuits to `null` for zero input tokens; the | ||
| // HoverCard content is closed by default, so the input row never renders. | ||
| expect(container.querySelector('[data-slot="input-usage"]')).toBeNull(); | ||
| // Explicitly, no "Input" label should leak out. | ||
| expect(screen.queryByText('Input')).toBeNull(); | ||
| }); | ||
|
|
||
| test('ContextOutputUsage renders nothing when outputTokens is 0', () => { | ||
| render( | ||
| <Context maxTokens={100} usage={zeroUsage} usedTokens={0}> | ||
| <ContextContent> | ||
| <ContextOutputUsage /> | ||
| </ContextContent> | ||
| </Context> | ||
| ); | ||
| expect(screen.queryByText('Output')).toBeNull(); | ||
| }); | ||
|
|
||
| test('ContextContentFooter renders $0.00 when no modelId is provided', () => { | ||
| // Without a modelId we can't look up per-token pricing; the footer should | ||
| // fall back to $0.00 (not NaN) even when usage is zero. | ||
| render( | ||
| <Context maxTokens={100} usage={zeroUsage} usedTokens={0}> | ||
| <ContextContent> | ||
| <ContextContentFooter /> | ||
| </ContextContent> | ||
| </Context> | ||
| ); | ||
| // Footer is inside a closed hover card by default, so we just assert no | ||
| // NaN / Infinity leaks out of the subtree. | ||
| const pretty = document.body.textContent ?? ''; | ||
| expect(pretty).not.toMatch(/NaN|Infinity/); | ||
| }); | ||
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Memoisation of provider value | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| describe('Context provider memoisation', () => { | ||
| test('provider value identity is stable across re-renders when props do not change', () => { | ||
| // We read the internal React context by swapping in a probe that captures | ||
| // the value reference seen on each render. Because Context's state | ||
| // container is module-private, we instead assert stability indirectly: | ||
| // the `useContextUsage`-style consumer would re-memoise its derived work | ||
| // from the stable value — we observe that here by checking the identity | ||
| // of the usage object we pass through remains referentially stable and | ||
| // that consumer renders the same count of text nodes. | ||
|
|
||
| const usage: LanguageModelUsage = { | ||
| inputTokens: 10, | ||
| outputTokens: 5, | ||
| totalTokens: 15, | ||
| }; | ||
|
|
||
| const { rerender, container } = render( | ||
| <Context maxTokens={100} usage={usage} usedTokens={15}> | ||
| <ContextTrigger /> | ||
| </Context> | ||
| ); | ||
| const firstTriggerText = container.textContent; | ||
| // Re-render with the exact same object references — no changes. | ||
| rerender( | ||
| <Context maxTokens={100} usage={usage} usedTokens={15}> | ||
| <ContextTrigger /> | ||
| </Context> | ||
| ); | ||
| expect(container.textContent).toBe(firstTriggerText); | ||
| }); | ||
|
|
||
| test('changing usage props produces a new derived output', () => { | ||
| const usageA: LanguageModelUsage = { | ||
| inputTokens: 10, | ||
| outputTokens: 5, | ||
| totalTokens: 15, | ||
| }; | ||
| const usageB: LanguageModelUsage = { | ||
| inputTokens: 20, | ||
| outputTokens: 10, | ||
| totalTokens: 30, | ||
| }; | ||
|
|
||
| const { rerender, container } = render( | ||
| <Context maxTokens={100} usage={usageA} usedTokens={15}> | ||
| <ContextTrigger /> | ||
| </Context> | ||
| ); | ||
| const first = container.textContent ?? ''; | ||
| rerender( | ||
| <Context maxTokens={100} usage={usageB} usedTokens={30}> | ||
| <ContextTrigger /> | ||
| </Context> | ||
| ); | ||
| const second = container.textContent ?? ''; | ||
| expect(first).not.toBe(second); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.