Skip to content

MastraCode web chat: render via standard Mastra message parts#18620

Open
mfrachet wants to merge 8 commits into
mainfrom
mastra-code-accumulator-mastra-db
Open

MastraCode web chat: render via standard Mastra message parts#18620
mfrachet wants to merge 8 commits into
mainfrom
mastra-code-accumulator-mastra-db

Conversation

@mfrachet

@mfrachet mfrachet commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

MastraCode's web chat previously rendered messages through a bespoke segment/tool model (AssistantSegment, ToolCall) that duplicated message-part rendering logic. This PR migrates the web transcript to render through @mastra/react's MessageFactory using the shared Mastra message-parts model.

A new controller-message accumulator converts AgentControllerMessage payloads from @mastra/client-js into standard MastraDBMessage objects (content.format: 2, content.parts, and content.metadata.harnessContent). The transcript reducer upserts these messages on stream events and on hydration, while keeping all non-message session state (approvals, suspensions, notifications, tasks, goals, subagents, usage, OM phase, local notices) unchanged.

Visible chat behavior is preserved: role labels (You / Steer / Agent / System), bubble styles, assistant markdown, thinking blocks, the streaming cursor, inline tool cards with expand/collapse, and the "Expand all (n)" toggle all behave as before. Harness/status metadata renders as compact status cards rather than raw content.

What changed

  • New agent-controller-message-accumulator.ts (+ unit tests) converting controller messages/events to MastraDBMessage parts.
  • Transcript reducer (transcript.ts) hydrates and upserts messages via the accumulator, overlaying runtime tool state for instant tool-card visibility.
  • components.tsx renders message parts via MessageFactory with role wrappers and per-part renderers (text, reasoning, tool-invocation, file) plus a status-metadata card path.
  • MSW-backed UI rendering tests and updated e2e scenarios for hydration, streaming, notifications, and SSE reconnect.
  • Changeset: mastracode patch — "Improved MastraCode web chat message rendering to use standard Mastra message parts."

Test plan

  • pnpm --filter ./mastracode check:ui (web UI typecheck) — passing
  • pnpm --filter ./mastracode test — all unit/MSW suites passing (incl. accumulator, transcript reducer, message-rendering MSW)
  • Manual: verify message hydration, streaming text, inline tool cards, expand/collapse, and status metadata rendering in the web Studio.

ELI5

This update makes MastraCode web chat understand and display its conversation history in a single, standard message format. That way, streaming updates, tool cards, and status info stay consistent whether the chat is just starting, hydrating from saved state, or reconnecting.

Summary

  • Switched MastraCode web chat transcript rendering to @mastra/react MessageFactory, using the shared Mastra message-parts model (MastraDBMessage with content.format: 2 + content.parts) instead of bespoke “segments/tool” rendering.
  • Added a controller-message accumulator (toMastraDBMessage) to convert AgentControllerMessage payloads into standard MastraDBMessage objects, including ordered tool-invocation parts and harness metadata via content.metadata.harnessContent.
  • Refactored the transcript model/reducer to use unified kind: "message" entries (MessageEntry) and upsert them during streaming/hydration, while preserving non-message session state (approvals, suspensions, notifications, tasks/goals/subagents, usage, OM phase, local notices).
  • Extended transcript handling for live tools by overlaying runtimeTools and inserting/updating corresponding tool-invocation parts inline inside the assistant/user message, including placeholder/tool-only assistant behavior when tool events arrive first.
  • Updated UI rendering (MessageBubble via MessageFactory) to keep existing visible behavior the same (role labels/bubbles, markdown, thinking blocks, streaming cursor, expand/collapse tool cards, “Expand all (n)” toggle) with harness/status metadata rendered as compact status cards.
  • Updated and expanded unit tests and e2e coverage for the new message-entry shape across hydration, streaming, notifications, and SSE reconnect; adjusted fixtures to match updated tool-call argument shapes.
  • Added a changeset documenting the improved hydrated/streamed message rendering behavior.

mfrachet and others added 2 commits June 29, 2026 13:58
Route the web transcript through @mastra/react MessageFactory using a
controller-message accumulator that converts AgentControllerMessage payloads
into MastraDBMessage parts. This replaces the bespoke segment/tool render path
so message rendering uses the shared Mastra parts model. Visible chat behavior
(role labels, bubbles, markdown, thinking, streaming cursor, inline tool cards,
expand/collapse) is preserved, and harness/status metadata renders as status
cards. Updates the colocated accumulator, transcript reducer, and MSW/e2e
coverage to the new representation.

Co-Authored-By: Mastra Code (anthropic/claude-opus-4-8) <noreply@mastra.ai>
@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mastra-playground-ui Ready Ready Preview, Comment Jun 29, 2026 5:18pm
mastra-studio-preview Ready Ready Preview, Comment Jun 29, 2026 5:18pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mastra-docs-1.x Ignored Ignored Preview Jun 29, 2026 5:18pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Replaces the transcript segments model with message entries backed by MastraDBMessage content parts, updates reducer and UI rendering to use the new shape, adjusts tests and e2e coverage, upgrades React to 19, and adds a patch changeset.

Changes

Unified message transcript model

Layer / File(s) Summary
Message conversion and transcript types
mastracode/src/web/ui/transcript.ts, mastracode/src/web/ui/agent-controller-message-accumulator.ts
Defines MessageEntry and ToolCall, updates TimelineEntry, and adds toMastraDBMessage to map controller messages into content.parts with harness metadata and tool invocation state.
Transcript hydration and tool state
mastracode/src/web/ui/transcript.ts
Rebuilds hydration, local user insertion, assistant upserts, and tool event handling around MessageEntry, runtimeTools, and message content parts.
MessageBubble rendering and App state
mastracode/src/web/ui/components.tsx, mastracode/src/web/ui/App.tsx, mastracode/src/web/ui/SettingsPanel.tsx, mastracode/package.json
Routes transcript rendering through MessageBubble and MessageFactory, hardens payload parsing, updates App streaming/indicator logic, adjusts React typing, and upgrades React plus @mastra/react.
Reducer and message conversion tests
mastracode/src/web/ui/transcript.test.ts, mastracode/src/web/ui/agent-controller-message-accumulator.test.ts
Adds tests for hydration, streaming, tool lifecycle, state preservation, content-part conversion, and harness metadata handling.
MSW-backed message rendering tests
mastracode/src/web/ui/__tests__/message-rendering.msw.test.tsx
Adds browser rendering tests covering persisted content, SSE streaming, tool lifecycle rendering, and status text output.
E2E driver, scenarios, and fixture
mastracode/e2e/web/driver.ts, mastracode/e2e/web/*.scenario.test.ts, mastracode/e2e/web/fixtures/plan-approval.json
Updates transcript flattening and scenario assertions to read kind === 'message' entries with message.content.parts, and adds a path field to the plan approval fixture.
Dependencies and release metadata
.changeset/thirty-candies-invite.md
Adds the patch changeset entry for the MastraCode release.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

tests: no tests added, complexity: high

Suggested reviewers

  • TylerBarnes
  • wardpeet
  • CalebBarnes
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change, describing the new standard Mastra message-part rendering path.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mastra-code-accumulator-mastra-db

Comment @coderabbitai help to get the list of available commands.

@changeset-bot

changeset-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1dd9c56

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
mastracode Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@socket-security

socket-security Bot commented Jun 29, 2026

Copy link
Copy Markdown

Dependency limit exceeded — report not shown.

This pull request scan exceeded the 10,000-dependency limit applied to this scan, so the results are incomplete and may be inaccurate. To avoid reporting false positives, Socket has not posted a report.

Upgrade your plan to raise the dependency limit and get complete reports, or view the partial scan in the dashboard.

Socket is always free for open source. If this is a non-commercial open source project, contact us to request a free Team account.

@mfrachet mfrachet marked this pull request as ready for review June 29, 2026 15:50
@mfrachet mfrachet requested a review from abhiaiyer91 June 29, 2026 15:50
@dane-ai-mastra

dane-ai-mastra Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

PR triage

Linked issue check skipped for core contributor @mfrachet.


PR complexity score

Factor Value Score impact
Files changed 17 +34
Lines changed 1357 +60
Author merged PRs 435 -20
Test files changed Yes -10
Final score 64

Applied label: complexity: critical


Changed test gate

Changed tests failed against the base branch as expected.

Label: tests: green ✅

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mastracode/src/web/ui/transcript.ts (1)

521-530: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

hydrate() still drops the live non-message state.

Lines 529-530 rebuild the reducer from initialTranscript, so any existing notices, prompts, subagents, tasks, goal state, and OM state vanish whenever hydrate runs on a populated session. That means reconnect hydration cannot preserve the non-message session state this reducer is supposed to carry forward.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mastracode/src/web/ui/transcript.ts` around lines 521 - 530, The hydrate()
helper is resetting the transcript to initialTranscript, which drops existing
non-message state during reconnect. Update hydrate() in transcript.ts to
preserve the current TranscriptState fields for notices, prompts, subagents,
tasks, goal, and OM-related state instead of rebuilding from initialTranscript,
while still replacing entries and the supplied
modeId/modelId/threadId/omProgress/usage values. Use the hydrate() function and
TranscriptState shape to locate the state merge logic.
🧹 Nitpick comments (2)
mastracode/src/web/ui/transcript.test.ts (1)

87-122: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extend this case to the re-emitted message_update.

The test stops right before the transition that regressed here: once a real assistant message_update arrives after tool_start/tool_end, the reducer should reuse the tool-only placeholder instead of leaving a second assistant entry behind. A follow-up assertion here would lock that behavior down.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mastracode/src/web/ui/transcript.test.ts` around lines 87 - 122, Extend the
transcriptReducer test to cover the real assistant message_update after
tool_start and tool_end: assert that the reducer reuses the existing tool-only
placeholder and does not leave a second assistant entry. Use the existing
messageParts helper and the tool lifecycle setup in transcript.test.ts so the
new assertion targets the same transcriptReducer behavior and the re-emitted
message_update path.
mastracode/src/web/ui/agent-controller-message-accumulator.test.ts (1)

47-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression that tool_result does not populate harnessContent.

The new metadata test only exercises status messages, so it would not catch assistant tool outputs being copied into content.metadata.harnessContent and doubling persisted payload size. A small assistant-message case here would pin that contract down.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mastracode/src/web/ui/agent-controller-message-accumulator.test.ts` around
lines 47 - 64, The new status-message test in toMastraDBMessage only covers
harness metadata for structured status content, so add a regression case in
agent-controller-message-accumulator.test.ts for an assistant message containing
tool_result content and verify that harnessContent is not set in
content.metadata while the rendered parts still serialize normally. Use the
existing toMastraDBMessage helper and AgentControllerMessage shape to locate the
relevant conversion path and pin down that tool outputs are not copied into
persisted harness metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/thirty-candies-invite.md:
- Line 5: The changeset entry is written as an internal implementation note
instead of a user-facing outcome; rewrite the summary in the changeset markdown
to describe the visible effect of the MastraCode web chat rendering update.
Focus on what users will notice in transcript/message display rather than
mentioning standard Mastra message parts or the refactor details.

In `@mastracode/e2e/web/streaming-text.scenario.test.ts`:
- Around line 22-23: The assertion in streaming-text.scenario.test.ts only fails
when last.streaming is truthy, so it can miss cases where message_end leaves the
flag undefined. Update the check around last.kind and last.streaming to
explicitly assert that the assistant entry from the message_end flow has
streaming === false, using the existing last variable in the test.

In `@mastracode/src/web/ui/__tests__/message-rendering.msw.test.tsx`:
- Around line 53-65: The SSE test helper in delayedSse currently leaves emit as
a no-op until ReadableStream.start runs, so the test can fire too early and miss
the update. Update the message-rendering MSW tests to wait for the stream to be
ready by using a real readiness signal from delayedSse before calling emit, and
apply the same fix anywhere the helper is reused in the message-rendering test
file so the SSE subscription is established before emitting events.

In `@mastracode/src/web/ui/agent-controller-message-accumulator.ts`:
- Around line 88-90: The isHarnessMetadataContent predicate in
agent-controller-message-accumulator.ts is incorrectly treating tool_result as
harness metadata, causing tool output to be duplicated in harnessContent. Update
that helper to explicitly exclude tool_result so only true metadata parts are
captured, while keeping text, thinking, and tool_call excluded as before. Verify
the AgentControllerMessageAccumulator path still feeds statusMetadata() the
notification/OM/error parts it expects from content.metadata.harnessContent.

In `@mastracode/src/web/ui/components.tsx`:
- Around line 294-310: The suspension payload parser in suspensionPayloadShape
is dropping the options field, so AskUserCard never receives prompt choices from
prompt.suspendPayload. Update this parser to read and վերադարձ the options
property alongside question, requestedPath, reason, title, and plan, using the
same safe property-access pattern already used in components.tsx so the payload
shape stays intact.
- Around line 627-637: Update toolFromInvocationPart so persisted failed tool
states are not treated as running: when toolInvocation.state is output-error or
output-denied, set ToolCall.status to error instead of running. Also carry any
available errorText from the ToolInvocationPart into the returned ToolCall so
failed tools render with their failure details, while preserving the current
runtime override behavior in toolFromInvocationPart.

In `@mastracode/src/web/ui/transcript.ts`:
- Around line 547-558: The assistant transcript logic is creating a separate
bubble for the real assistant message instead of merging it with the earlier
tool-only placeholder. Update upsertAssistant in transcript.ts to detect and
reuse the synthesized assistant-tools placeholder entry (not just entries with
the same message.id), so the later message_update replaces that entry and
preserves its runtimeTools. Use the existing toMessageEntry,
preserveRuntimeToolParts, and assistant-tools entry shape to reconcile the
placeholder into the first real assistant message.

---

Outside diff comments:
In `@mastracode/src/web/ui/transcript.ts`:
- Around line 521-530: The hydrate() helper is resetting the transcript to
initialTranscript, which drops existing non-message state during reconnect.
Update hydrate() in transcript.ts to preserve the current TranscriptState fields
for notices, prompts, subagents, tasks, goal, and OM-related state instead of
rebuilding from initialTranscript, while still replacing entries and the
supplied modeId/modelId/threadId/omProgress/usage values. Use the hydrate()
function and TranscriptState shape to locate the state merge logic.

---

Nitpick comments:
In `@mastracode/src/web/ui/agent-controller-message-accumulator.test.ts`:
- Around line 47-64: The new status-message test in toMastraDBMessage only
covers harness metadata for structured status content, so add a regression case
in agent-controller-message-accumulator.test.ts for an assistant message
containing tool_result content and verify that harnessContent is not set in
content.metadata while the rendered parts still serialize normally. Use the
existing toMastraDBMessage helper and AgentControllerMessage shape to locate the
relevant conversion path and pin down that tool outputs are not copied into
persisted harness metadata.

In `@mastracode/src/web/ui/transcript.test.ts`:
- Around line 87-122: Extend the transcriptReducer test to cover the real
assistant message_update after tool_start and tool_end: assert that the reducer
reuses the existing tool-only placeholder and does not leave a second assistant
entry. Use the existing messageParts helper and the tool lifecycle setup in
transcript.test.ts so the new assertion targets the same transcriptReducer
behavior and the re-emitted message_update path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 847ed42a-e6ab-41b4-a40e-97ae6c415477

📥 Commits

Reviewing files that changed from the base of the PR and between d527fa5 and 5d8df2d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • .changeset/thirty-candies-invite.md
  • mastracode/e2e/web/driver.ts
  • mastracode/e2e/web/fixtures/plan-approval.json
  • mastracode/e2e/web/notification.scenario.test.ts
  • mastracode/e2e/web/sse-reconnect.scenario.test.ts
  • mastracode/e2e/web/streaming-text.scenario.test.ts
  • mastracode/e2e/web/transcript-hydrate.scenario.test.ts
  • mastracode/package.json
  • mastracode/src/web/ui/App.tsx
  • mastracode/src/web/ui/SettingsPanel.tsx
  • mastracode/src/web/ui/__tests__/message-rendering.msw.test.tsx
  • mastracode/src/web/ui/agent-controller-message-accumulator.test.ts
  • mastracode/src/web/ui/agent-controller-message-accumulator.ts
  • mastracode/src/web/ui/components.tsx
  • mastracode/src/web/ui/transcript.test.ts
  • mastracode/src/web/ui/transcript.ts

Comment thread .changeset/thirty-candies-invite.md Outdated
Comment thread mastracode/e2e/web/streaming-text.scenario.test.ts Outdated
Comment thread mastracode/src/web/ui/__tests__/message-rendering.msw.test.tsx Outdated
Comment thread mastracode/src/web/ui/agent-controller-message-accumulator.ts
Comment thread mastracode/src/web/ui/components.tsx
Comment thread mastracode/src/web/ui/components.tsx
Comment thread mastracode/src/web/ui/transcript.ts
…-ai/mastra into mastra-code-accumulator-mastra-db

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mastracode/src/web/ui/components.tsx`:
- Line 317: The suspension payload mapping in the component that builds the
controller payload should preserve the target path instead of only reading
requestedPath. Update the payload extraction logic in the relevant
dispatcher/card builder so it also reads payload.path as a fallback (alongside
requestedPath) when constructing the object used by the access/plan prompt flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 89f3da0d-c65f-488a-8996-f929fe5535ee

📥 Commits

Reviewing files that changed from the base of the PR and between 5d8df2d and 1dd9c56.

📒 Files selected for processing (7)
  • .changeset/thirty-candies-invite.md
  • mastracode/e2e/web/streaming-text.scenario.test.ts
  • mastracode/e2e/web/transcript-hydrate.scenario.test.ts
  • mastracode/src/web/ui/__tests__/message-rendering.msw.test.tsx
  • mastracode/src/web/ui/agent-controller-message-accumulator.ts
  • mastracode/src/web/ui/components.tsx
  • mastracode/src/web/ui/transcript.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/thirty-candies-invite.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • mastracode/e2e/web/streaming-text.scenario.test.ts
  • mastracode/src/web/ui/agent-controller-message-accumulator.ts
  • mastracode/src/web/ui/tests/message-rendering.msw.test.tsx
  • mastracode/e2e/web/transcript-hydrate.scenario.test.ts
  • mastracode/src/web/ui/transcript.ts

return {
question: stringProperty(payload, 'question'),
options,
requestedPath: stringProperty(payload, 'requestedPath'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve path from suspension payloads.

Line 317 only reads requestedPath, but the suspension payload flow passes the controller payload through unchanged and the existing dispatcher contract uses payload.path for access/plan prompts. Add path as a fallback so those cards don’t lose the target path.

Proposed fix
-    requestedPath: stringProperty(payload, 'requestedPath'),
+    requestedPath: stringProperty(payload, 'requestedPath') ?? stringProperty(payload, 'path'),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
requestedPath: stringProperty(payload, 'requestedPath'),
requestedPath: stringProperty(payload, 'requestedPath') ?? stringProperty(payload, 'path'),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mastracode/src/web/ui/components.tsx` at line 317, The suspension payload
mapping in the component that builds the controller payload should preserve the
target path instead of only reading requestedPath. Update the payload extraction
logic in the relevant dispatcher/card builder so it also reads payload.path as a
fallback (alongside requestedPath) when constructing the object used by the
access/plan prompt flow.

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

Labels

complexity: critical Critical-complexity PR tests: green ✅ Changed tests failed against base as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants