diff --git a/frontend/src/components/StagePanel.test.tsx b/frontend/src/components/StagePanel.test.tsx
index e46489cd..a0fe5a05 100644
--- a/frontend/src/components/StagePanel.test.tsx
+++ b/frontend/src/components/StagePanel.test.tsx
@@ -1,4 +1,5 @@
import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import type { PipelineStageItem } from "../api/client";
import { StagePanel } from "./StagePanel";
@@ -489,6 +490,50 @@ describe("StagePanel", () => {
expect(icon).toBeInTheDocument();
expect(icon).toHaveTextContent("⏳");
});
+
+ it("renders gate output in a bounded box with expand and copy controls", () => {
+ const items: PipelineStageItem[] = [
+ {
+ story_id: "60_story_gate_output",
+ name: "Gate Output Story",
+ error: null,
+ merge_failure: "Quality gates failed: cargo test output here",
+ agent: null,
+ review_hold: null,
+ qa: null,
+ depends_on: null,
+ },
+ ];
+ render();
+ expect(screen.getByTestId("gate-output-text")).toHaveTextContent(
+ "Quality gates failed: cargo test output here",
+ );
+ expect(screen.getByTestId("gate-output-toggle")).toBeInTheDocument();
+ expect(screen.getByTestId("gate-output-copy")).toBeInTheDocument();
+ });
+
+ it("expand toggle changes label from Expand to Collapse", async () => {
+ const user = userEvent.setup();
+ const items: PipelineStageItem[] = [
+ {
+ story_id: "61_story_expand",
+ name: "Expand Story",
+ error: null,
+ merge_failure: "A".repeat(1000),
+ agent: null,
+ review_hold: null,
+ qa: null,
+ depends_on: null,
+ },
+ ];
+ render();
+ const toggle = screen.getByTestId("gate-output-toggle");
+ expect(toggle).toHaveTextContent("Expand");
+ await user.click(toggle);
+ expect(toggle).toHaveTextContent("Collapse");
+ await user.click(toggle);
+ expect(toggle).toHaveTextContent("Expand");
+ });
});
describe("StagePanel - defensive rendering", () => {
diff --git a/frontend/src/components/StagePanel.tsx b/frontend/src/components/StagePanel.tsx
index e3845350..5faebb43 100644
--- a/frontend/src/components/StagePanel.tsx
+++ b/frontend/src/components/StagePanel.tsx
@@ -5,6 +5,82 @@ import { useLozengeFly } from "./LozengeFlyContext";
const { useLayoutEffect, useRef, useState } = React;
+/** Renders merge-failure gate output in a bounded scroll region with expand and copy controls. */
+function GateOutputBox({ text }: { text: string }) {
+ const [expanded, setExpanded] = useState(false);
+ const [copied, setCopied] = useState(false);
+
+ const handleToggle = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setExpanded((prev) => !prev);
+ };
+
+ const handleCopy = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ navigator.clipboard.writeText(text).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ });
+ };
+
+ const btnStyle: React.CSSProperties = {
+ background: "transparent",
+ border: "1px solid #444",
+ borderRadius: "4px",
+ color: "#aaa",
+ cursor: "pointer",
+ fontSize: "0.75em",
+ padding: "1px 6px",
+ lineHeight: 1.4,
+ };
+
+ return (
+
+
+ {text}
+
+
+
+
+
+
+ );
+}
+
type WorkItemType = "story" | "bug" | "spike" | "refactor" | "unknown";
const TYPE_COLORS: Record = {
@@ -564,15 +640,8 @@ export function StagePanel({
{item.merge_failure && (
- {item.merge_failure}
+
)}
{item.depends_on && item.depends_on.length > 0 && (