From 2131ede7b8df75bbf5d2e0ad6a81a5a9d95d7e78 Mon Sep 17 00:00:00 2001
From: Chris Farhood
Date: Wed, 29 Apr 2026 14:22:31 -0400
Subject: [PATCH] feat(board): render approval
summary/recommendedAction/nextActionOnApproval as markdown
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replaces plain tags in BoardApprovalPayloadContent with MarkdownBody
(softBreaks enabled) so agent-authored markdown in these three fields —
headers, bullets, and newlines — renders correctly in the Board UI instead
of collapsing into a single unstyled paragraph. No schema change; the
fields remain plain strings in the approval payload, only the renderer
changed. Matches how comments, issue documents, and interaction cards
already render markdown via MarkdownBody.
Test coverage added for ## header →
, bullet list → - , and
plain-prose regression (no markup injected for single-line inputs).
Co-Authored-By: Claude Sonnet 4.6
---
ui/src/components/ApprovalPayload.test.tsx | 151 ++++++++++++++++++---
ui/src/components/ApprovalPayload.tsx | 7 +-
2 files changed, 135 insertions(+), 23 deletions(-)
diff --git a/ui/src/components/ApprovalPayload.test.tsx b/ui/src/components/ApprovalPayload.test.tsx
index c11405e9..1b082e06 100644
--- a/ui/src/components/ApprovalPayload.test.tsx
+++ b/ui/src/components/ApprovalPayload.test.tsx
@@ -1,13 +1,33 @@
// @vitest-environment jsdom
import { act } from "react";
+import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
-import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { ThemeProvider } from "../context/ThemeContext";
import { ApprovalPayloadRenderer, approvalLabel } from "./ApprovalPayload";
+vi.mock("@/lib/router", () => ({
+ Link: ({ children, to }: { children: ReactNode; to: string }) => {children},
+}));
+
+vi.mock("../api/issues", () => ({
+ issuesApi: { get: vi.fn() },
+}));
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+function withProviders(children: ReactNode) {
+ return (
+
+ {children}
+
+ );
+}
+
describe("approvalLabel", () => {
it("uses payload titles for generic board approvals", () => {
expect(
@@ -35,17 +55,19 @@ describe("ApprovalPayloadRenderer", () => {
act(() => {
root.render(
- ,
+ withProviders(
+ ,
+ ),
);
});
@@ -67,14 +89,16 @@ describe("ApprovalPayloadRenderer", () => {
act(() => {
root.render(
- ,
+ withProviders(
+ ,
+ ),
);
});
@@ -86,3 +110,90 @@ describe("ApprovalPayloadRenderer", () => {
});
});
});
+
+describe("BoardApprovalPayloadContent markdown rendering", () => {
+ it("renders a ## header in summary as an h2 element", () => {
+ const html = renderToStaticMarkup(
+ withProviders(
+ ,
+ ),
+ );
+ expect(html).toContain("
{
+ const html = renderToStaticMarkup(
+ withProviders(
+ ,
+ ),
+ );
+ expect(html).toContain(" {
+ const html = renderToStaticMarkup(
+ withProviders(
+ ,
+ ),
+ );
+ expect(html).toContain(" {
+ const html = renderToStaticMarkup(
+ withProviders(
+ ,
+ ),
+ );
+ expect(html).toContain(" {
+ const html = renderToStaticMarkup(
+ withProviders(
+ ,
+ ),
+ );
+ expect(html).toContain("This is a simple one-line summary.");
+ expect(html).not.toContain(" {
+ const html = renderToStaticMarkup(
+ withProviders(
+ ,
+ ),
+ );
+ expect(html).toContain("Approve the deployment.");
+ expect(html).not.toContain(" = {
hire_agent: "Hire Agent",
@@ -185,7 +186,7 @@ function BoardApprovalPayloadContent({ payload }: { payload: Record
Summary
- {summary}
+ {summary}
)}
{recommendedAction && (
@@ -193,13 +194,13 @@ function BoardApprovalPayloadContent({ payload }: { payload: Record
Recommended action
- {recommendedAction}
+ {recommendedAction}
)}
{nextActionOnApproval && (
On approval
-
{nextActionOnApproval}
+
{nextActionOnApproval}
)}
{risks.length > 0 && (